From 5928afc3bf83661cd3b11130a31a2d97ef135a9e Mon Sep 17 00:00:00 2001 From: Jokler Date: Sat, 4 Nov 2017 22:46:39 +0100 Subject: Add MySql as a possible database for the Factoids plugin --- migrations/.gitkeep | 0 migrations/2017-11-03-164322_create_factoids/down.sql | 1 + migrations/2017-11-03-164322_create_factoids/up.sql | 4 ++++ 3 files changed, 5 insertions(+) create mode 100644 migrations/.gitkeep create mode 100644 migrations/2017-11-03-164322_create_factoids/down.sql create mode 100644 migrations/2017-11-03-164322_create_factoids/up.sql (limited to 'migrations') diff --git a/migrations/.gitkeep b/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2017-11-03-164322_create_factoids/down.sql b/migrations/2017-11-03-164322_create_factoids/down.sql new file mode 100644 index 0000000..58c2110 --- /dev/null +++ b/migrations/2017-11-03-164322_create_factoids/down.sql @@ -0,0 +1 @@ +DROP TABLE factoids diff --git a/migrations/2017-11-03-164322_create_factoids/up.sql b/migrations/2017-11-03-164322_create_factoids/up.sql new file mode 100644 index 0000000..84e9f9d --- /dev/null +++ b/migrations/2017-11-03-164322_create_factoids/up.sql @@ -0,0 +1,4 @@ +CREATE TABLE factoids ( + name VARCHAR(32) PRIMARY KEY, + content VARCHAR(5000) NOT NULL +) -- cgit v1.2.3-70-g09d2 From 4b5693d3c6781a5ca4ef32f43f3994a65020c933 Mon Sep 17 00:00:00 2001 From: Jokler Date: Sat, 16 Dec 2017 18:13:02 +0100 Subject: Add more info to factoids and save old versions --- Cargo.lock | 78 +++++++++-- Cargo.toml | 19 ++- bin/main.rs | 30 ++++- migrations/.gitkeep | 0 .../2017-11-03-164322_create_factoids/up.sql | 8 +- src/lib.rs | 9 +- src/plugins/factoids/database.rs | 120 +++++++++++------ src/plugins/factoids/mod.rs | 146 +++++++++++++++++---- 8 files changed, 322 insertions(+), 88 deletions(-) delete mode 100644 migrations/.gitkeep (limited to 'migrations') diff --git a/Cargo.lock b/Cargo.lock index 91693d5..936d8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,32 +220,40 @@ dependencies = [ [[package]] name = "diesel" -version = "0.16.0" +version = "1.0.0-rc1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "diesel_derives 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", "mysqlclient-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "diesel_codegen" -version = "0.16.0" +name = "diesel_derives" +version = "1.0.0-rc1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "diesel_infer_schema 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "diesel_infer_schema" -version = "0.16.0" +version = "1.0.0-rc1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "infer_schema_macros 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "diesel_migrations" +version = "1.0.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "migrations_internals 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", + "migrations_macros 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -350,9 +358,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" name = "frippy" version = "0.3.1" dependencies = [ + "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "clippy 0.0.177 (registry+https://github.com/rust-lang/crates.io-index)", - "diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "diesel_codegen 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", + "diesel 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", + "diesel_infer_schema 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", + "diesel_migrations 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", "dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "frippy_derive 0.1.0", "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", @@ -502,6 +512,25 @@ name = "if_chain" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "infer_schema_internals" +version = "1.0.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "diesel 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "infer_schema_macros" +version = "1.0.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "infer_schema_internals 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "iovec" version = "0.1.1" @@ -628,6 +657,24 @@ dependencies = [ "libc 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "migrations_internals" +version = "1.0.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "diesel 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "migrations_macros" +version = "1.0.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "migrations_internals 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "mime" version = "0.3.5" @@ -1389,9 +1436,10 @@ dependencies = [ "checksum dbghelp-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "97590ba53bcb8ac28279161ca943a924d1fd4a8fb3fa63302591647c4fc5b850" "checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3" "checksum derive-error-chain 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c9ca9ade651388daad7c993f005d0d20c4f6fe78c1cdc93e95f161c6f5ede4a" -"checksum diesel 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "304226fa7a3982b0405f6bb95dd9c10c3e2000709f194038a60ec2c277150951" -"checksum diesel_codegen 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18a42ca5c9b660add51d58bc5a50a87123380e1e458069c5504528a851ed7384" -"checksum diesel_infer_schema 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bf1957ff5cd3b04772e43c162c2f69c2aa918080ff9b020276792d236be8be52" +"checksum diesel 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "f6b9e512c7fbcc7240848252ff620ed5f0e996e25d5c694b2b535d27db7465c6" +"checksum diesel_derives 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "a631faea061a7b5ab85e842866da57dbc31cc5fe715397a5e5a5b0effd7146b9" +"checksum diesel_infer_schema 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "1cee2c5104f0258a4461c4c8694e870161cb974f4de8609fae3d496e26336590" +"checksum diesel_migrations 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "e1fd168d237ae0bd9e8bfdcb82ba87a4b72e4c1d9edd864242a67149c7f3c960" "checksum dotenv 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d6f0e2bb24d163428d8031d3ebd2d2bd903ad933205a97d0f18c7c1aade380f3" "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum either 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "740178ddf48b1a9e878e6d6509a1442a2d42fd2928aae8e7a6f8a36fb01981b3" @@ -1419,6 +1467,8 @@ dependencies = [ "checksum hyper-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9c81fa95203e2a6087242c38691a0210f23e9f3f8f944350bd676522132e2985" "checksum idna 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "014b298351066f1512874135335d62a789ffe78a9974f94b43ed5621951eaf7d" "checksum if_chain 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "61bb90bdd39e3af69b0172dfc6130f6cd6332bf040fbb9bdd4401d37adbd48b8" +"checksum infer_schema_internals 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "e2af8e23352c51aff14633500bfff361e9ea310375044b40dd8939e2defccd5c" +"checksum infer_schema_macros 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa05e92983b3f9af1e7508f7d4981452542d505640d1b328fe74f4466c82b953" "checksum iovec 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b6e8b9c2247fcf6c6a1151f1156932be5606c9fd6f55a2d7f9fc1cb29386b2f7" "checksum irc 0.12.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c5053c25509042e963baa67d0d648add9f72b7487784505a5d5a3054d136a615" "checksum itertools 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "d3f2be4da1690a039e9ae5fd575f706a63ad5a2120f161b1d653c9da3930dd21" @@ -1435,6 +1485,8 @@ dependencies = [ "checksum markup5ever 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff834ac7123c6a37826747e5ca09db41fd7a83126792021c2e636ad174bb77d3" "checksum matches 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "100aabe6b8ff4e4a7e32c1c13523379802df0772b82466207ac25b013f193376" "checksum memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" +"checksum migrations_internals 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "c39531d07a48b5920d19310f9ba644667aac56edd933a6e7649893551756ad50" +"checksum migrations_macros 1.0.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)" = "12d6c72b7dae5fb40009c6313ef5a2075aa25f479a8a51f6a487abef887a7a2c" "checksum mime 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e2e00e17be181010a91dbfefb01660b17311059dc8c7f48b9017677721e732bd" "checksum mime_guess 2.0.0-alpha.3 (registry+https://github.com/rust-lang/crates.io-index)" = "013572795763289e14710c7b279461295f2673b2b338200c235082cd7ca9e495" "checksum mio 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "0e8411968194c7b139e9105bc4ae7db0bae232af087147e72f0616ebf5fdb9cb" diff --git a/Cargo.toml b/Cargo.toml index 5ff779f..5f535e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ path = "bin/main.rs" doc = false [features] -mysql = ["diesel", "diesel_codegen", "dotenv"] +mysql = ["diesel", "diesel_infer_schema", "diesel_migrations", "dotenv"] [dependencies] irc = "0.12.5" @@ -27,13 +27,14 @@ tokio-core = "0.1.10" futures = "0.1.16" log = "0.3.8" time = "0.1" -rlua = "0.9.2" +rlua = "0.9.3" reqwest = "0.8.0" select = "0.4.2" regex = "0.2.2" lazy_static = "0.2.9" serde = "1.0.15" serde_json = "1.0.3" +chrono = "0.4.0" glob = "0.2" frippy_derive = { path = "frippy_derive" } @@ -42,13 +43,19 @@ frippy_derive = { path = "frippy_derive" } git = 'https://github.com/Jokler/unicode_names' branch = 'update-to-latest-unicode' + [dependencies.diesel] -version = "0.16.0" +version = "1.0.0-beta1" +optional = true +features = ["mysql", "chrono"] + +[dependencies.diesel_infer_schema] +version = "1.0.0-beta1" optional = true features = ["mysql"] -[dependencies.diesel_codegen] -version = "0.16.0" +[dependencies.diesel_migrations] +version = "1.0.0-beta1" optional = true features = ["mysql"] @@ -58,4 +65,4 @@ optional = true [dependencies.clippy] version = "*" -optional = true \ No newline at end of file +optional = true diff --git a/bin/main.rs b/bin/main.rs index 1597a70..7869548 100644 --- a/bin/main.rs +++ b/bin/main.rs @@ -4,9 +4,16 @@ extern crate tokio_core; extern crate glob; extern crate futures; +#[cfg(feature = "mysql")] +#[macro_use] +extern crate diesel_migrations; +#[cfg(feature = "mysql")] +extern crate diesel; + #[macro_use] extern crate log; +#[cfg(not(feature = "mysql"))] use std::collections::HashMap; use log::{LogRecord, LogLevel, LogLevelFilter, LogMetadata}; @@ -18,6 +25,9 @@ use glob::glob; use frippy::plugins; use frippy::Config; +#[cfg(feature = "mysql")] +embed_migrations!(); + struct Logger; impl log::Log for Logger { @@ -44,7 +54,6 @@ impl log::Log for Logger { } fn main() { - let log_level = if cfg!(debug_assertions) { LogLevelFilter::Debug } else { @@ -87,7 +96,10 @@ fn main() { let mut disabled_plugins = None; if let &Some(ref options) = &config.options { if let Some(disabled) = options.get("disabled_plugins") { - disabled_plugins = Some(disabled.split(",").map(|p| p.trim()).collect::>()); + disabled_plugins = Some(disabled + .split(",") + .map(|p| p.trim()) + .collect::>()); } } @@ -97,8 +109,22 @@ fn main() { bot.add_plugin(plugins::Emoji::new()); bot.add_plugin(plugins::Currency::new()); bot.add_plugin(plugins::KeepNick::new()); + #[cfg(feature = "mysql")] + { + use diesel; + use diesel::Connection; + match diesel::mysql::MysqlConnection::establish("mysql://user:password@address/db") { + Ok(conn) => { + embedded_migrations::run(&conn).unwrap(); + bot.add_plugin(plugins::Factoids::new(conn)); + } + Err(e) => error!("Failed to connect to database: {}", e), + } + } + #[cfg(not(feature = "mysql"))] bot.add_plugin(plugins::Factoids::new(HashMap::new())); + if let Some(disabled_plugins) = disabled_plugins { for name in disabled_plugins { if let None = bot.remove_plugin(name) { diff --git a/migrations/.gitkeep b/migrations/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/2017-11-03-164322_create_factoids/up.sql b/migrations/2017-11-03-164322_create_factoids/up.sql index 84e9f9d..784a3f4 100644 --- a/migrations/2017-11-03-164322_create_factoids/up.sql +++ b/migrations/2017-11-03-164322_create_factoids/up.sql @@ -1,4 +1,8 @@ CREATE TABLE factoids ( - name VARCHAR(32) PRIMARY KEY, - content VARCHAR(5000) NOT NULL + name VARCHAR(32) NOT NULL, + idx INTEGER NOT NULL, + content TEXT NOT NULL, + author VARCHAR(32) NOT NULL, + created TIMESTAMP NOT NULL, + PRIMARY KEY (name, idx) ) diff --git a/src/lib.rs b/src/lib.rs index 1f5514e..4407e6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,10 @@ #[macro_use] extern crate diesel; #[cfg(feature = "mysql")] +extern crate diesel_infer_schema; +#[cfg(feature = "mysql")] #[macro_use] -extern crate diesel_codegen; +extern crate diesel_migrations; #[macro_use] extern crate log; @@ -48,12 +50,15 @@ extern crate frippy_derive; extern crate irc; extern crate futures; extern crate tokio_core; +extern crate regex; +extern crate chrono; +extern crate time; pub mod plugin; pub mod plugins; -use std::fmt; use std::collections::HashMap; +use std::fmt; use std::thread::spawn; use std::sync::Arc; diff --git a/src/plugins/factoids/database.rs b/src/plugins/factoids/database.rs index dbf136c..522c9d0 100644 --- a/src/plugins/factoids/database.rs +++ b/src/plugins/factoids/database.rs @@ -9,66 +9,112 @@ 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_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, name: &str, content: &str) -> Option<()>; - fn get(&self, name: &str) -> Option; + fn insert(&mut self, factoid: &NewFactoid) -> DbResponse; + fn get(&self, name: &str, idx: i32) -> Option; + fn count(&self, name: &str) -> Result; } -impl Database for HashMap { - fn insert(&mut self, name: &str, content: &str) -> Option<()> { - self.insert(String::from(name), String::from(content)).map(|_| ()) +// 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) -> Option { - self.get(name).cloned() + fn get(&self, name: &str, idx: i32) -> Option { + self.get(&(String::from(name), idx)).map(|f| f.clone()) } -} -#[cfg(feature = "mysql")] -#[derive(Queryable)] -struct Factoid { - pub name: String, - pub content: String, + fn count(&self, name: &str) -> Result { + Ok(self.iter() + .filter(|&(&(ref n, _), _)| n == name) + .count() as i32) + } } +// MySql #[cfg(feature = "mysql")] table! { - factoids (name) { + factoids (name, idx) { name -> Varchar, - content -> Varchar, + idx -> Integer, + content -> Text, + author -> Varchar, + created -> Timestamp, } } -#[cfg(feature = "mysql")] -#[derive(Insertable)] -#[table_name="factoids"] -struct NewFactoid<'a> { - pub name: &'a str, - pub content: &'a str, -} - - #[cfg(feature = "mysql")] impl Database for MysqlConnection { - fn insert(&mut self, name: &str, content: &str) -> Option<()> { - let factoid = NewFactoid { - name: name, - content: content, - }; + fn insert(&mut self, factoid: &NewFactoid) -> DbResponse { + use diesel; - ::diesel::insert(&factoid) - .into(factoids::table) - .execute(self) - .ok() - .map(|_| ()) + match diesel::insert_into(factoids::table) + .values(factoid) + .execute(self) { + Ok(_) => DbResponse::Success, + Err(_) => DbResponse::Failed("Database error - possible duplicate"), + } } - fn get(&self, name: &str) -> Option { + fn get(&self, name: &str, idx: i32) -> Option { factoids::table - .filter(factoids::columns::name.eq(name)) + .find((name, idx)) .limit(1) .load::(self) .ok() - .and_then(|v| v.first().map(|f| f.content.clone())) + .and_then(|v| v.into_iter().next()) + } + + fn count(&self, name: &str) -> Result { + let count: Result = 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 index 5f9f99a..bb83700 100644 --- a/src/plugins/factoids/mod.rs +++ b/src/plugins/factoids/mod.rs @@ -1,15 +1,18 @@ 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::Error as IrcError; -use std::sync::Mutex; +use time; +use chrono::NaiveDateTime; use plugin::*; mod database; -use self::database::Database; +use self::database::{Database, DbResponse}; static LUA_SANDBOX: &'static str = include_str!("sandbox.lua"); @@ -33,50 +36,140 @@ impl Factoids { } fn add(&self, server: &IrcServer, command: &mut PluginCommand) -> Result<(), IrcError> { - if command.tokens.len() < 2 { return self.invalid_command(server, command); } let name = command.tokens.remove(0); - - try_lock!(self.factoids) - .insert(&name, &command.tokens.join(" ")); - - server.send_notice(&command.source, "Successfully added") + let content = command.tokens.join(" "); + let count = match try_lock!(self.factoids).count(&name) { + Ok(c) => c, + Err(e) => return server.send_notice(&command.source, e), + }; + + let tm = time::now().to_timespec(); + + let factoid = database::NewFactoid { + name: &name, + idx: count, + content: &content, + author: &command.source, + created: NaiveDateTime::from_timestamp(tm.sec, tm.nsec as u32), + }; + + match try_lock!(self.factoids).insert(&factoid) { + DbResponse::Success => server.send_notice(&command.source, "Successfully added"), + DbResponse::Failed(e) => server.send_notice(&command.source, &e), + } } fn get(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { - if command.tokens.len() < 1 { - self.invalid_command(server, command) + let (name, idx) = match command.tokens.len() { + 0 => return self.invalid_command(server, command), + 1 => { + let name = &command.tokens[0]; + let count = match try_lock!(self.factoids).count(name) { + Ok(c) => c, + Err(e) => return server.send_notice(&command.source, e), + }; + + if count < 1 { + return server.send_notice(&command.source, &format!("{} does not exist", name)); + } - } else { - let name = &command.tokens[0]; - let factoids = try_lock!(self.factoids); - let factoid = match factoids.get(name) { - Some(v) => v, - None => return self.invalid_command(server, command), - }; + (name, count - 1) + } + _ => { + let name = &command.tokens[0]; + let idx = match i32::from_str(&command.tokens[1]) { + Ok(i) => i, + Err(_) => return server.send_notice(&command.source, "Invalid index"), + }; + + (name, idx) + } + }; + + let factoid = match try_lock!(self.factoids).get(name, idx) { + Some(v) => v, + None => return server.send_notice(&command.source, &format!("{}~{} does not exist", name, idx)), + }; + + server.send_privmsg(&command.target, + &format!("{}: {}", factoid.name, factoid.content)) + } + + fn info(&self, server: &IrcServer, command: &PluginCommand) -> Result<(), IrcError> { + + match command.tokens.len() { + 0 => self.invalid_command(server, command), + 1 => { + let name = &command.tokens[0]; + let count = match try_lock!(self.factoids).count(name) { + Ok(c) => c, + Err(e) => return server.send_notice(&command.source, e), + }; + + match count { + 0 => server.send_notice(&command.source, &format!("{} does not exist", name)), + 1 => { + server.send_privmsg(&command.target, + &format!("There is 1 version of {}", name)) + } + _ => { + server.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 server.send_notice(&command.source, "Invalid index"), + }; + + let factoid = match try_lock!(self.factoids).get(name, idx) { + Some(v) => v, + None => { + return server.send_notice(&command.source, + &format!("{}~{} does not exist", name, idx)) + } + }; + + server.send_privmsg(&command.target, + &format!("{}: Added by {} at {} UTC", + name, + factoid.author, + factoid.created)) + } - server.send_privmsg(&command.target, &format!("{}: {}", name, factoid)) } } - fn exec(&self, server: &IrcServer, mut command: PluginCommand) -> Result<(), IrcError> { + fn exec(&self, + server: &IrcServer, + mut command: PluginCommand, + error: bool) + -> Result<(), IrcError> { if command.tokens.len() < 1 { self.invalid_command(server, &command) } else { let name = command.tokens.remove(0); + let count = match try_lock!(self.factoids).count(&name) { + Ok(c) => c, + Err(e) => return server.send_notice(&command.source, e), + }; - let factoids = try_lock!(self.factoids); - let factoid = match factoids.get(&name) { - Some(v) => v, - None => return self.invalid_command(server, &command), + let factoid = match try_lock!(self.factoids).get(&name, count - 1) { + Some(v) => v.content, + None if error => return self.invalid_command(server, &command), + None => return Ok(()), }; - let value = if factoid.starts_with(">") { + let value = &if factoid.starts_with(">") { let factoid = String::from(&factoid[1..]); if factoid.starts_with(">") { @@ -149,7 +242,7 @@ impl Plugin for Factoids { tokens: t, }; - self.exec(server, c) + self.exec(server, c, false) } else { Ok(()) @@ -165,7 +258,8 @@ impl Plugin for Factoids { match sub_command.as_ref() { "add" => self.add(server, &mut command), "get" => self.get(server, &command), - "exec" => self.exec(server, command), + "info" => self.info(server, &command), + "exec" => self.exec(server, command, true), _ => self.invalid_command(server, &command), } } -- cgit v1.2.3-70-g09d2 From 92668ad4c53edcc1a317a16aa5ea30ca502f54ee Mon Sep 17 00:00:00 2001 From: Jokler Date: Sun, 25 Feb 2018 18:54:16 +0100 Subject: Add Mysql as a possible database to the Tell plugin --- migrations/2018-02-25-154922_create_tells/down.sql | 1 + migrations/2018-02-25-154922_create_tells/up.sql | 7 + src/main.rs | 10 +- src/plugins/factoids/database.rs | 15 ++- src/plugins/factoids/mod.rs | 2 +- src/plugins/tell.rs | 143 -------------------- src/plugins/tell/database.rs | 143 ++++++++++++++++++++ src/plugins/tell/mod.rs | 144 +++++++++++++++++++++ 8 files changed, 313 insertions(+), 152 deletions(-) create mode 100644 migrations/2018-02-25-154922_create_tells/down.sql create mode 100644 migrations/2018-02-25-154922_create_tells/up.sql delete mode 100644 src/plugins/tell.rs create mode 100644 src/plugins/tell/database.rs create mode 100644 src/plugins/tell/mod.rs (limited to 'migrations') diff --git a/migrations/2018-02-25-154922_create_tells/down.sql b/migrations/2018-02-25-154922_create_tells/down.sql new file mode 100644 index 0000000..73c9b9b --- /dev/null +++ b/migrations/2018-02-25-154922_create_tells/down.sql @@ -0,0 +1 @@ +DROP TABLE tells diff --git a/migrations/2018-02-25-154922_create_tells/up.sql b/migrations/2018-02-25-154922_create_tells/up.sql new file mode 100644 index 0000000..9de037c --- /dev/null +++ b/migrations/2018-02-25-154922_create_tells/up.sql @@ -0,0 +1,7 @@ +CREATE TABLE tells ( + id SERIAL PRIMARY KEY, + sender VARCHAR(32) NOT NULL, + receiver VARCHAR(32) NOT NULL, + time TIMESTAMP NOT NULL, + message VARCHAR(512) NOT NULL +) diff --git a/src/main.rs b/src/main.rs index 511fe09..ec04d33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,8 @@ extern crate r2d2_diesel; #[macro_use] extern crate log; +#[cfg(feature = "mysql")] +use std::sync::Arc; use std::collections::HashMap; use log::{Level, LevelFilter, Metadata, Record}; @@ -115,7 +117,6 @@ fn main() { bot.add_plugin(plugins::Emoji::new()); bot.add_plugin(plugins::Currency::new()); bot.add_plugin(plugins::KeepNick::new()); - bot.add_plugin(plugins::Tell::new()); #[cfg(feature = "mysql")] { @@ -130,11 +131,14 @@ fn main() { .expect("Failed to get connection")) { Ok(_) => { - bot.add_plugin(plugins::Factoids::new(pool)); + let pool = Arc::new(pool); + bot.add_plugin(plugins::Factoids::new(pool.clone())); + bot.add_plugin(plugins::Tell::new(pool.clone())); info!("Connected to MySQL server") } Err(e) => { bot.add_plugin(plugins::Factoids::new(HashMap::new())); + bot.add_plugin(plugins::Tell::new(HashMap::new())); error!("Failed to run migrations: {}", e); } }, @@ -142,6 +146,7 @@ fn main() { } } else { bot.add_plugin(plugins::Factoids::new(HashMap::new())); + bot.add_plugin(plugins::Tell::new(HashMap::new())); } } #[cfg(not(feature = "mysql"))] @@ -150,6 +155,7 @@ fn main() { error!("frippy was not built with the mysql feature") } bot.add_plugin(plugins::Factoids::new(HashMap::new())); + bot.add_plugin(plugins::Tell::new(HashMap::new())); } if let Some(disabled_plugins) = disabled_plugins { diff --git a/src/plugins/factoids/database.rs b/src/plugins/factoids/database.rs index 386d3f7..88aa0fd 100644 --- a/src/plugins/factoids/database.rs +++ b/src/plugins/factoids/database.rs @@ -1,6 +1,8 @@ #[cfg(feature = "mysql")] extern crate dotenv; +#[cfg(feature = "mysql")] +use std::sync::Arc; use std::collections::HashMap; #[cfg(feature = "mysql")] @@ -29,8 +31,6 @@ pub struct Factoid { 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> { @@ -82,10 +82,10 @@ impl Database for HashMap<(String, i32), Factoid> { } } -// Diesel automatically define the factoids module as public. -// For now this is how we keep it private. +// Diesel automatically defines the factoids module as public. +// We create a schema module to keep it private. #[cfg(feature = "mysql")] -mod mysql { +mod schema { table! { factoids (name, idx) { name -> Varchar, @@ -98,7 +98,10 @@ mod mysql { } #[cfg(feature = "mysql")] -impl Database for Pool> { +use self::schema::factoids; + +#[cfg(feature = "mysql")] +impl Database for Arc>> { fn insert_factoid(&mut self, factoid: &NewFactoid) -> DbResponse { use diesel; diff --git a/src/plugins/factoids/mod.rs b/src/plugins/factoids/mod.rs index c3d19b0..d6abb1d 100644 --- a/src/plugins/factoids/mod.rs +++ b/src/plugins/factoids/mod.rs @@ -49,7 +49,7 @@ impl Factoids { idx: count, content: content, author: author, - created: NaiveDateTime::from_timestamp(tm.sec, tm.nsec as u32), + created: NaiveDateTime::from_timestamp(tm.sec, 0u32), }; match try_lock!(self.factoids).insert_factoid(&factoid) { diff --git a/src/plugins/tell.rs b/src/plugins/tell.rs deleted file mode 100644 index 89d91f2..0000000 --- a/src/plugins/tell.rs +++ /dev/null @@ -1,143 +0,0 @@ -use irc::client::prelude::*; -use irc::error::IrcError; - -use std::collections::HashMap; -use std::sync::Mutex; - -use plugin::*; - -macro_rules! try_lock { - ( $m:expr ) => { - match $m.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - } - } -} - -#[derive(PluginName, Default, Debug)] -pub struct Tell { - tells: Mutex>>, -} - -#[derive(Default, Debug)] -struct TellMessage { - sender: String, - // TODO Add time - message: String, -} - -impl Tell { - pub fn new() -> Tell { - Tell { - tells: Mutex::new(HashMap::new()), - } - } - - fn tell_command(&self, client: &IrcClient, command: &PluginCommand) -> Result<&str, String> { - if command.tokens.len() < 2 { - return Err(self.invalid_command(client)); - } - - let receiver = command.tokens[0].to_string(); - let sender = command.source.to_owned(); - - if receiver == sender { - return Err(String::from("That's your name!")); - } - - if command.source != command.target { - if let Some(users) = client.list_users(&command.target) { - if users.iter().any(|u| u.get_nickname() == receiver) { - return Err(format!("{} is in this channel.", receiver)); - } - } - } - - let message = command.tokens[1..].join(" "); - let tell = TellMessage { - sender: sender, - message: message, - }; - - let mut tells = try_lock!(self.tells); - let tell_messages = tells - .entry(receiver) - .or_insert_with(|| Vec::with_capacity(3)); - (*tell_messages).push(tell); - - Ok("Got it!") - } - - fn send_tell(&self, client: &IrcClient, receiver: &str) -> ExecutionStatus { - let mut tells = try_lock!(self.tells); - if let Some(tell_messages) = tells.get_mut(receiver) { - for tell in tell_messages { - if let Err(e) = client.send_notice( - receiver, - &format!("Tell from {}: {}", tell.sender, tell.message), - ) { - return ExecutionStatus::Err(Box::new(e)); - } - debug!( - "Sent {:?} from {:?} to {:?}", - tell.message, tell.sender, receiver - ); - } - } - tells.remove(receiver); - ExecutionStatus::Done - } - - fn invalid_command(&self, client: &IrcClient) -> String { - format!( - "Incorrect Command. \ - Send \"{} tell help\" for help.", - client.current_nickname() - ) - } - - fn help(&self, client: &IrcClient) -> String { - format!( - "usage: {} tell user message\r\n\ - example: {0} tell Foobar Hello!", - client.current_nickname() - ) - } -} - -impl Plugin for Tell { - fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { - match message.command { - Command::JOIN(_, _, _) | Command::PRIVMSG(_, _) => { - self.send_tell(client, message.source_nickname().unwrap()) - } - _ => ExecutionStatus::Done, - } - } - - fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { - panic!("Tell should not use threading") - } - - fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { - if command.tokens.is_empty() { - return client.send_notice(&command.source, &self.invalid_command(client)); - } - - match command.tokens[0].as_ref() { - "help" => client.send_notice(&command.source, &self.help(client)), - _ => match self.tell_command(client, &command) { - Ok(msg) => client.send_notice(&command.source, msg), - Err(msg) => client.send_notice(&command.source, &msg), - }, - } - } - - fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result { - Err(String::from("This Plugin does not implement any commands.")) - } -} - -#[cfg(test)] -mod tests {} diff --git a/src/plugins/tell/database.rs b/src/plugins/tell/database.rs new file mode 100644 index 0000000..277847e --- /dev/null +++ b/src/plugins/tell/database.rs @@ -0,0 +1,143 @@ +#[cfg(feature = "mysql")] +extern crate dotenv; + +#[cfg(feature = "mysql")] +use std::sync::Arc; +use std::collections::HashMap; + +#[cfg(feature = "mysql")] +use diesel::prelude::*; +#[cfg(feature = "mysql")] +use diesel::mysql::MysqlConnection; +#[cfg(feature = "mysql")] +use r2d2::Pool; +#[cfg(feature = "mysql")] +use r2d2_diesel::ConnectionManager; + +use chrono::NaiveDateTime; + +pub enum DbResponse { + Success, + Failed(&'static str), +} + +#[cfg_attr(feature = "mysql", derive(Queryable))] +#[derive(PartialEq, Clone, Debug)] +pub struct TellMessage { + pub id: i64, + pub sender: String, + pub receiver: String, + pub time: NaiveDateTime, + pub message: String, +} + +#[cfg_attr(feature = "mysql", derive(Insertable))] +#[cfg_attr(feature = "mysql", table_name = "tells")] +pub struct NewTellMessage<'a> { + pub sender: &'a str, + pub receiver: &'a str, + pub time: NaiveDateTime, + pub message: &'a str, +} + +pub trait Database: Send { + fn insert_tell(&mut self, tell: &NewTellMessage) -> DbResponse; + fn get_tells(&self, receiver: &str) -> Option>; + fn delete_tells(&mut self, receiver: &str) -> DbResponse; +} + +// HashMap +impl Database for HashMap> { + fn insert_tell(&mut self, tell: &NewTellMessage) -> DbResponse { + let tell = TellMessage { + id: 0, + sender: tell.sender.to_string(), + receiver: tell.receiver.to_string(), + time: tell.time, + message: tell.message.to_string(), + }; + + let receiver = tell.receiver.clone(); + let tell_messages = self.entry(receiver) + .or_insert_with(|| Vec::with_capacity(3)); + (*tell_messages).push(tell); + + DbResponse::Success + } + + fn get_tells(&self, receiver: &str) -> Option> { + self.get(receiver).cloned() + } + + fn delete_tells(&mut self, receiver: &str) -> DbResponse { + match self.remove(receiver) { + Some(_) => DbResponse::Success, + None => DbResponse::Failed("Tells not found"), + } + } +} + +// Diesel automatically defines the tells module as public. +// We create a schema module to keep it private. +#[cfg(feature = "mysql")] +mod schema { + table! { + tells (id) { + id -> Bigint, + sender -> Varchar, + receiver -> Varchar, + time -> Timestamp, + message -> Varchar, + } + } +} + +#[cfg(feature = "mysql")] +use self::schema::tells; + +#[cfg(feature = "mysql")] +impl Database for Arc>> { + fn insert_tell(&mut self, tell: &NewTellMessage) -> DbResponse { + use diesel; + + let conn = &*self.get().expect("Failed to get connection"); + match diesel::insert_into(tells::table).values(tell).execute(conn) { + Ok(_) => DbResponse::Success, + Err(e) => { + error!("DB failed to insert tell: {}", e); + DbResponse::Failed("Failed to save Tell") + } + } + } + + fn get_tells(&self, receiver: &str) -> Option> { + use self::tells::columns; + + let conn = &*self.get().expect("Failed to get connection"); + match tells::table + .filter(columns::receiver.eq(receiver)) + .order(columns::time.asc()) + .load::(conn) + { + Ok(f) => Some(f), + Err(e) => { + error!("DB failed to get tells: {}", e); + None + } + } + } + + fn delete_tells(&mut self, receiver: &str) -> DbResponse { + use diesel; + use self::tells::columns; + + let conn = &*self.get().expect("Failed to get connection"); + match diesel::delete(tells::table.filter(columns::receiver.eq(receiver))).execute(conn) { + Ok(_) => DbResponse::Success, + Err(e) => { + error!("DB failed to delete tells: {}", e); + DbResponse::Failed("Failed to delete tells") + } + } + } +} diff --git a/src/plugins/tell/mod.rs b/src/plugins/tell/mod.rs new file mode 100644 index 0000000..1a1bef1 --- /dev/null +++ b/src/plugins/tell/mod.rs @@ -0,0 +1,144 @@ +use irc::client::prelude::*; +use irc::error::IrcError; + +use time; +use chrono::NaiveDateTime; +use std::sync::Mutex; + +use plugin::*; + +pub mod database; +use self::database::{Database, DbResponse}; + +macro_rules! try_lock { + ( $m:expr ) => { + match $m.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } +} + +#[derive(PluginName, Default)] +pub struct Tell { + tells: Mutex, +} + +impl Tell { + pub fn new(db: T) -> Tell { + Tell { + tells: Mutex::new(db), + } + } + + fn tell_command(&self, client: &IrcClient, command: &PluginCommand) -> Result<&str, String> { + if command.tokens.len() < 2 { + return Err(self.invalid_command(client)); + } + + let receiver = command.tokens[0].to_string(); + let sender = command.source.to_owned(); + + if receiver == sender { + return Err(String::from("That's your name!")); + } + + if command.source != command.target { + if let Some(users) = client.list_users(&command.target) { + if users.iter().any(|u| u.get_nickname() == receiver) { + return Err(format!("{} is in this channel.", receiver)); + } + } + } + + let tm = time::now().to_timespec(); + let message = command.tokens[1..].join(" "); + let tell = database::NewTellMessage { + sender: &sender, + receiver: &receiver, + time: NaiveDateTime::from_timestamp(tm.sec, 0u32), + message: &message, + }; + + match try_lock!(self.tells).insert_tell(&tell) { + DbResponse::Success => Ok("Got it!"), + DbResponse::Failed(e) => Err(e.to_string()), + } + } + + fn send_tell(&self, client: &IrcClient, receiver: &str) -> ExecutionStatus { + let mut tells = try_lock!(self.tells); + if let Some(tell_messages) = tells.get_tells(receiver) { + for tell in tell_messages { + if let Err(e) = client.send_notice( + receiver, + &format!("Tell from {}: {}", tell.sender, tell.message), + ) { + return ExecutionStatus::Err(Box::new(e)); + } + debug!( + "Sent {:?} from {:?} to {:?}", + tell.message, tell.sender, receiver + ); + } + } + tells.delete_tells(receiver); + ExecutionStatus::Done + } + + fn invalid_command(&self, client: &IrcClient) -> String { + format!( + "Incorrect Command. \ + Send \"{} tell help\" for help.", + client.current_nickname() + ) + } + + fn help(&self, client: &IrcClient) -> String { + format!( + "usage: {} tell user message\r\n\ + example: {0} tell Foobar Hello!", + client.current_nickname() + ) + } +} + +impl Plugin for Tell { + fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { + match message.command { + Command::JOIN(_, _, _) | Command::PRIVMSG(_, _) => { + self.send_tell(client, message.source_nickname().unwrap()) + } + _ => ExecutionStatus::Done, + } + } + + fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), IrcError> { + panic!("Tell should not use threading") + } + + fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), IrcError> { + if command.tokens.is_empty() { + return client.send_notice(&command.source, &self.invalid_command(client)); + } + + match command.tokens[0].as_ref() { + "help" => client.send_notice(&command.source, &self.help(client)), + _ => match self.tell_command(client, &command) { + Ok(msg) => client.send_notice(&command.source, msg), + Err(msg) => client.send_notice(&command.source, &msg), + }, + } + } + + fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result { + Err(String::from("This Plugin does not implement any commands.")) + } +} + +use std::fmt; +impl fmt::Debug for Tell { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Tell {{ ... }}") + } +} -- cgit v1.2.3-70-g09d2