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 --- 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 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 305 insertions(+), 152 deletions(-) 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 'src') 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