aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJokler <jokler.contact@gmail.com>2018-09-21 00:21:43 +0200
committerJokler <jokler.contact@gmail.com>2018-09-21 00:21:43 +0200
commit625ca41cf54bac0268f7bde9d7ad9017c03d5919 (patch)
treeae32e4e08972488904027d3b57ed9ec9f97f871e
parent448b52d250c61fb719477c01c633593c3da68fba (diff)
downloadfrippy-625ca41cf54bac0268f7bde9d7ad9017c03d5919.tar.gz
frippy-625ca41cf54bac0268f7bde9d7ad9017c03d5919.zip
Quote: Add initial quote plugin
-rw-r--r--Cargo.lock1
-rw-r--r--Cargo.toml1
-rw-r--r--migrations/2018-09-19-231843_create_quotes/down.sql1
-rw-r--r--migrations/2018-09-19-231843_create_quotes/up.sql9
-rw-r--r--src/error.rs4
-rw-r--r--src/lib.rs1
-rw-r--r--src/main.rs5
-rw-r--r--src/plugins/factoids/mod.rs2
-rw-r--r--src/plugins/help.rs2
-rw-r--r--src/plugins/mod.rs1
-rw-r--r--src/plugins/quote/database.rs136
-rw-r--r--src/plugins/quote/mod.rs262
12 files changed, 423 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index a3fcb18..d5c4cc4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -416,6 +416,7 @@ dependencies = [
"log4rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"r2d2-diesel 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rlua 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/Cargo.toml b/Cargo.toml
index a8c264f..a400bf3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,7 @@ antidote = "1.0.0"
log4rs = "0.8.0"
frippy_derive = { path = "frippy_derive" }
+rand = "0.5.5"
# TODO Use crates.io again, as soon as OpenSSL 1.1.1 is supported
[dependencies.irc]
diff --git a/migrations/2018-09-19-231843_create_quotes/down.sql b/migrations/2018-09-19-231843_create_quotes/down.sql
new file mode 100644
index 0000000..b9d0f8b
--- /dev/null
+++ b/migrations/2018-09-19-231843_create_quotes/down.sql
@@ -0,0 +1 @@
+DROP TABLE quotes
diff --git a/migrations/2018-09-19-231843_create_quotes/up.sql b/migrations/2018-09-19-231843_create_quotes/up.sql
new file mode 100644
index 0000000..357a63a
--- /dev/null
+++ b/migrations/2018-09-19-231843_create_quotes/up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE quotes (
+ quotee VARCHAR(32) NOT NULL,
+ channel VARCHAR(32) NOT NULL,
+ idx INTEGER NOT NULL,
+ content TEXT NOT NULL,
+ author VARCHAR(32) NOT NULL,
+ created TIMESTAMP NOT NULL,
+ PRIMARY KEY (quotee, channel, idx)
+)
diff --git a/src/error.rs b/src/error.rs
index 251e812..70a7724 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -33,6 +33,10 @@ pub enum ErrorKind {
#[fail(display = "A Factoids error has occured")]
Factoids,
+ /// A Quote error
+ #[fail(display = "A Quote error has occured")]
+ Quote,
+
/// A Remind error
#[fail(display = "A Remind error has occured")]
Remind,
diff --git a/src/lib.rs b/src/lib.rs
index b8662e4..c257544 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -54,6 +54,7 @@ extern crate regex;
extern crate reqwest;
extern crate serde_json;
extern crate time;
+extern crate rand;
pub mod error;
pub mod plugin;
diff --git a/src/main.rs b/src/main.rs
index 9aed069..c337a46 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -31,6 +31,7 @@ use irc::client::reactor::IrcReactor;
use frippy::plugins::emoji::Emoji;
use frippy::plugins::factoids::Factoids;
+use frippy::plugins::quote::Quote;
use frippy::plugins::help::Help;
use frippy::plugins::keepnick::KeepNick;
use frippy::plugins::remind::Remind;
@@ -122,12 +123,14 @@ fn run() -> Result<(), Error> {
Ok(_) => {
let pool = Arc::new(pool);
bot.add_plugin(Factoids::new(pool.clone()));
+ bot.add_plugin(Quote::new(pool.clone()));
bot.add_plugin(Tell::new(pool.clone()));
bot.add_plugin(Remind::new(pool.clone()));
info!("Connected to MySQL server")
}
Err(e) => {
bot.add_plugin(Factoids::new(HashMap::new()));
+ bot.add_plugin(Quote::new(HashMap::new()));
bot.add_plugin(Tell::new(HashMap::new()));
bot.add_plugin(Remind::new(HashMap::new()));
error!("Failed to run migrations: {}", e);
@@ -137,6 +140,7 @@ fn run() -> Result<(), Error> {
}
} else {
bot.add_plugin(Factoids::new(HashMap::new()));
+ bot.add_plugin(Quote::new(HashMap::new()));
bot.add_plugin(Tell::new(HashMap::new()));
bot.add_plugin(Remind::new(HashMap::new()));
}
@@ -147,6 +151,7 @@ fn run() -> Result<(), Error> {
error!("frippy was not built with the mysql feature")
}
bot.add_plugin(Factoids::new(HashMap::new()));
+ bot.add_plugin(Quote::new(HashMap::new()));
bot.add_plugin(Tell::new(HashMap::new()));
bot.add_plugin(Remind::new(HashMap::new()));
}
diff --git a/src/plugins/factoids/mod.rs b/src/plugins/factoids/mod.rs
index 10f5131..a3d521a 100644
--- a/src/plugins/factoids/mod.rs
+++ b/src/plugins/factoids/mod.rs
@@ -228,7 +228,7 @@ impl<T: Database, C: Client> Factoids<T, C> {
fn help(&self) -> &str {
"usage: factoids <subcommand>\r\n\
- subcommands: add, fromurl, remove, info, get, exec, help"
+ subcommands: add, fromurl, remove, get, info, exec, help"
}
}
diff --git a/src/plugins/help.rs b/src/plugins/help.rs
index 5de2aca..1b50ca0 100644
--- a/src/plugins/help.rs
+++ b/src/plugins/help.rs
@@ -36,7 +36,7 @@ impl<C: FrippyClient> Plugin for Help<C> {
client
.send_notice(
&command.source,
- "Available commands: help, tell, factoids, remind\r\n\
+ "Available commands: help, tell, factoids, remind, quote\r\n\
For more detailed help call help on the specific command.\r\n\
Example: 'remind help'",
)
diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs
index 05616f1..e3bda60 100644
--- a/src/plugins/mod.rs
+++ b/src/plugins/mod.rs
@@ -1,6 +1,7 @@
//! Collection of plugins included
pub mod emoji;
pub mod factoids;
+pub mod quote;
pub mod help;
pub mod keepnick;
pub mod remind;
diff --git a/src/plugins/quote/database.rs b/src/plugins/quote/database.rs
new file mode 100644
index 0000000..6ad08a0
--- /dev/null
+++ b/src/plugins/quote/database.rs
@@ -0,0 +1,136 @@
+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 Quote {
+ pub quotee: String,
+ pub channel: 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 = "quotes")]
+pub struct NewQuote<'a> {
+ pub quotee: &'a str,
+ pub channel: &'a str,
+ pub idx: i32,
+ pub content: &'a str,
+ pub author: &'a str,
+ pub created: NaiveDateTime,
+}
+
+pub trait Database: Send + Sync {
+ fn insert_quote(&mut self, quote: &NewQuote) -> Result<(), QuoteError>;
+ fn get_quote(&self, quotee: &str, channel: &str, idx: i32) -> Result<Quote, QuoteError>;
+ fn count_quotes(&self, quotee: &str, channel: &str) -> Result<i32, QuoteError>;
+}
+
+// HashMap
+impl<S: ::std::hash::BuildHasher + Send + Sync> Database for HashMap<(String, String, i32), Quote, S> {
+ fn insert_quote(&mut self, quote: &NewQuote) -> Result<(), QuoteError> {
+ let quote = Quote {
+ quotee: quote.quotee.to_owned(),
+ channel: quote.channel.to_owned(),
+ idx: quote.idx,
+ content: quote.content.to_owned(),
+ author: quote.author.to_owned(),
+ created: quote.created,
+ };
+
+ let quotee = quote.quotee.clone();
+ let channel = quote.channel.clone();
+ match self.insert((quotee, channel, quote.idx), quote) {
+ None => Ok(()),
+ Some(_) => Err(ErrorKind::Duplicate)?,
+ }
+ }
+
+ fn get_quote(&self, quotee: &str, channel: &str, idx: i32) -> Result<Quote, QuoteError> {
+ Ok(self.get(&(quotee.to_owned(), channel.to_owned(), idx))
+ .cloned()
+ .ok_or(ErrorKind::NotFound)?)
+ }
+
+ fn count_quotes(&self, quotee: &str, channel: &str) -> Result<i32, QuoteError> {
+ Ok(self.iter().filter(|&(&(ref n, ref c, _), _)| n == quotee && c == channel).count() as i32)
+ }
+}
+
+// Diesel automatically defines the quotes module as public.
+// We create a schema module to keep it private.
+#[cfg(feature = "mysql")]
+mod schema {
+ table! {
+ quotes (quotee, channel, idx) {
+ quotee -> Varchar,
+ channel -> Varchar,
+ idx -> Integer,
+ content -> Text,
+ author -> Varchar,
+ created -> Timestamp,
+ }
+ }
+}
+
+#[cfg(feature = "mysql")]
+use self::schema::quotes;
+
+#[cfg(feature = "mysql")]
+impl Database for Arc<Pool<ConnectionManager<MysqlConnection>>> {
+ fn insert_quote(&mut self, quote: &NewQuote) -> Result<(), QuoteError> {
+ use diesel;
+
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ diesel::insert_into(quotes::table)
+ .values(quote)
+ .execute(conn)
+ .context(ErrorKind::MysqlError)?;
+
+ Ok(())
+ }
+
+ fn get_quote(&self, quotee: &str, channel: &str, idx: i32) -> Result<Quote, QuoteError> {
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ Ok(quotes::table
+ .find((quotee, channel, idx))
+ .first(conn)
+ .context(ErrorKind::MysqlError)?)
+ }
+
+ fn count_quotes(&self, quotee: &str, channel: &str) -> Result<i32, QuoteError> {
+ use diesel;
+
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ let count: Result<i64, _> = quotes::table
+ .filter(quotes::columns::quotee.eq(quotee))
+ .filter(quotes::columns::channel.eq(channel))
+ .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/quote/mod.rs b/src/plugins/quote/mod.rs
new file mode 100644
index 0000000..43333e7
--- /dev/null
+++ b/src/plugins/quote/mod.rs
@@ -0,0 +1,262 @@
+use std::fmt;
+use std::marker::PhantomData;
+use std::str::FromStr;
+
+use antidote::RwLock;
+use irc::client::prelude::*;
+use rand::{thread_rng, Rng};
+use chrono::NaiveDateTime;
+use time;
+
+use plugin::*;
+use FrippyClient;
+pub mod database;
+use self::database::Database;
+
+use self::error::*;
+use error::ErrorKind as FrippyErrorKind;
+use error::FrippyError;
+use failure::ResultExt;
+
+enum QuoteResponse {
+ Public(String),
+ Private(String),
+}
+
+#[derive(PluginName)]
+pub struct Quote<T: Database, C: Client> {
+ quotes: RwLock<T>,
+ phantom: PhantomData<C>,
+}
+
+impl<T: Database, C: Client> Quote<T, C> {
+ pub fn new(db: T) -> Self {
+ Quote {
+ quotes: RwLock::new(db),
+ phantom: PhantomData,
+ }
+ }
+
+ fn create_quote(
+ &self,
+ quotee: &str,
+ channel: &str,
+ content: &str,
+ author: &str,
+ ) -> Result<&str, QuoteError> {
+ let count = self.quotes.read().count_quotes(quotee, channel)?;
+ let tm = time::now().to_timespec();
+
+ let quote = database::NewQuote {
+ quotee,
+ channel,
+ idx: count + 1,
+ content,
+ author,
+ created: NaiveDateTime::from_timestamp(tm.sec, 0u32),
+ };
+
+ Ok(self.quotes
+ .write()
+ .insert_quote(&quote)
+ .map(|()| "Successfully added!")?)
+ }
+
+ fn add(&self, command: &mut PluginCommand) -> Result<&str, QuoteError> {
+ if command.tokens.len() < 2 {
+ Err(ErrorKind::InvalidCommand)?;
+ }
+
+ if command.target == command.source {
+ Err(ErrorKind::PrivateMessageNotAllowed)?;
+ }
+
+ let quotee = command.tokens.remove(0);
+ let channel = &command.target;
+ let content = command.tokens.join(" ");
+
+ Ok(self.create_quote(&quotee, channel, &content, &command.source)?)
+ }
+
+ fn get(&self, command: &PluginCommand) -> Result<String, QuoteError> {
+ if command.tokens.is_empty() {
+ Err(ErrorKind::InvalidCommand)?;
+ }
+
+ let quotee = &command.tokens[0];
+ let channel = &command.target;
+ let count = self.quotes.read().count_quotes(quotee, channel)?;
+
+ if count < 1 {
+ Err(ErrorKind::NotFound)?;
+ }
+
+ let idx = match command.tokens.len() {
+ 1 => thread_rng().gen_range(1, count + 1),
+ _ => {
+ let idx = match i32::from_str(&command.tokens[1]) {
+ Ok(i) => i,
+ Err(_) => Err(ErrorKind::InvalidIndex)?,
+ };
+
+ if idx < 0 {
+ count + idx + 1
+ } else {
+ idx
+ }
+ }
+ };
+
+ let quote = self.quotes
+ .read()
+ .get_quote(quotee, channel, idx)
+ .context(ErrorKind::NotFound)?;
+
+
+ Ok(format!("\"{}\" - {}[{}/{}]", quote.content, quote.quotee, idx, count))
+ }
+
+ fn info(&self, command: &PluginCommand) -> Result<String, QuoteError> {
+ match command.tokens.len() {
+ 0 => Err(ErrorKind::InvalidCommand)?,
+ 1 => {
+ let quotee = &command.tokens[0];
+ let channel = &command.target;
+ let count = self.quotes.read().count_quotes(quotee, channel)?;
+
+ Ok(match count {
+ 0 => Err(ErrorKind::NotFound)?,
+ 1 => format!("{} has 1 quote", quotee),
+ _ => format!("{} has {} quotes", quotee, count),
+ })
+ }
+ _ => {
+ let quotee = &command.tokens[0];
+ let channel = &command.target;
+ let idx = i32::from_str(&command.tokens[1]).context(ErrorKind::InvalidIndex)?;
+ let quote = self.quotes.read().get_quote(quotee, channel, idx)?;
+
+ Ok(format!(
+ "{}'s quote was added by {} at {} UTC",
+ quotee, quote.author, quote.created
+ ))
+ }
+ }
+ }
+
+ fn help(&self) -> &str {
+ "usage: quotes <subcommand>\r\n\
+ subcommands: add, get, info, help"
+ }
+}
+
+impl<T: Database, C: FrippyClient> Plugin for Quote<T, C> {
+ type Client = C;
+ fn execute(&self, _: &Self::Client, _: &Message) -> ExecutionStatus {
+ ExecutionStatus::Done
+ }
+
+ fn execute_threaded(&self, _: &Self::Client, _: &Message) -> Result<(), FrippyError> {
+ panic!("Quotes should not use threading")
+ }
+
+ fn command(
+ &self,
+ client: &Self::Client,
+ mut command: PluginCommand,
+ ) -> Result<(), FrippyError> {
+ use self::QuoteResponse::{Private, Public};
+
+ if command.tokens.is_empty() {
+ client
+ .send_notice(&command.source, &ErrorKind::InvalidCommand.to_string())
+ .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())),
+ "get" => self.get(&command).map(Public),
+ "info" => self.info(&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::Quote)?
+ }
+ }
+
+ Ok(())
+ }
+
+ fn evaluate(&self, _: &Self::Client, _: PluginCommand) -> Result<String, String> {
+ Err(String::from(
+ "Evaluation of commands is not implemented for Quote at this time",
+ ))
+ }
+}
+
+impl<T: Database, C: FrippyClient> fmt::Debug for Quote<T, C> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Quote {{ ... }}")
+ }
+}
+
+pub mod error {
+ #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail, Error)]
+ #[error = "QuoteError"]
+ pub enum ErrorKind {
+ /// Invalid command error
+ #[fail(display = "Incorrect command. Send \"quote help\" for help")]
+ InvalidCommand,
+
+ /// Invalid index error
+ #[fail(display = "Invalid index")]
+ InvalidIndex,
+
+ /// Private message error
+ #[fail(display = "You can only add quotes in channel messages")]
+ PrivateMessageNotAllowed,
+
+ /// Download error
+ #[fail(display = "Download failed")]
+ Download,
+
+ /// Duplicate error
+ #[fail(display = "Entry already exists")]
+ Duplicate,
+
+ /// Not found error
+ #[fail(display = "Quote 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,
+ }
+}