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/plugins/tell/database.rs | 143 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/plugins/tell/database.rs (limited to 'src/plugins/tell/database.rs') 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") + } + } + } +} -- cgit v1.2.3-70-g09d2 From 095af339c035bc750993318311c9a35ea455e9a7 Mon Sep 17 00:00:00 2001 From: Jokler Date: Mon, 5 Mar 2018 16:13:45 +0100 Subject: Add Tell specific errors and improve error logging for commands --- src/lib.rs | 2 +- src/plugins/tell/database.rs | 72 ++++++++++++--------------- src/plugins/tell/mod.rs | 114 +++++++++++++++++++++++++++++-------------- 3 files changed, 110 insertions(+), 78 deletions(-) (limited to 'src/plugins/tell/database.rs') diff --git a/src/lib.rs b/src/lib.rs index a740ec7..ebadb86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -228,7 +228,7 @@ impl ThreadedPlugins { // Send the message to the plugin if the plugin needs it match plugin.execute(client, &message) { ExecutionStatus::Done => (), - ExecutionStatus::Err(e) => error!("Error in {} - {}", name, e), + ExecutionStatus::Err(e) => log_error(e), ExecutionStatus::RequiresThread => { debug!( "Spawning thread to execute {} with {}", diff --git a/src/plugins/tell/database.rs b/src/plugins/tell/database.rs index 277847e..40ec6fc 100644 --- a/src/plugins/tell/database.rs +++ b/src/plugins/tell/database.rs @@ -16,10 +16,10 @@ use r2d2_diesel::ConnectionManager; use chrono::NaiveDateTime; -pub enum DbResponse { - Success, - Failed(&'static str), -} +#[cfg(feature = "mysql")] +use failure::ResultExt; + +use super::error::*; #[cfg_attr(feature = "mysql", derive(Queryable))] #[derive(PartialEq, Clone, Debug)] @@ -41,14 +41,14 @@ pub struct NewTellMessage<'a> { } 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; + fn insert_tell(&mut self, tell: &NewTellMessage) -> Result<(), TellError>; + fn get_tells(&self, receiver: &str) -> Result, TellError>; + fn delete_tells(&mut self, receiver: &str) -> Result<(), TellError>; } // HashMap impl Database for HashMap> { - fn insert_tell(&mut self, tell: &NewTellMessage) -> DbResponse { + fn insert_tell(&mut self, tell: &NewTellMessage) -> Result<(), TellError> { let tell = TellMessage { id: 0, sender: tell.sender.to_string(), @@ -62,17 +62,17 @@ impl Database for HashMap> { .or_insert_with(|| Vec::with_capacity(3)); (*tell_messages).push(tell); - DbResponse::Success + Ok(()) } - fn get_tells(&self, receiver: &str) -> Option> { - self.get(receiver).cloned() + fn get_tells(&self, receiver: &str) -> Result, TellError> { + Ok(self.get(receiver).cloned().ok_or(ErrorKind::NotFound)?) } - fn delete_tells(&mut self, receiver: &str) -> DbResponse { + fn delete_tells(&mut self, receiver: &str) -> Result<(), TellError> { match self.remove(receiver) { - Some(_) => DbResponse::Success, - None => DbResponse::Failed("Tells not found"), + Some(_) => Ok(()), + None => Err(ErrorKind::NotFound)?, } } } @@ -97,47 +97,37 @@ use self::schema::tells; #[cfg(feature = "mysql")] impl Database for Arc>> { - fn insert_tell(&mut self, tell: &NewTellMessage) -> DbResponse { + fn insert_tell(&mut self, tell: &NewTellMessage) -> Result<(), TellError> { 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") - } - } + diesel::insert_into(tells::table) + .values(tell) + .execute(conn) + .context(ErrorKind::MysqlError)?; + + Ok(()) } - fn get_tells(&self, receiver: &str) -> Option> { + fn get_tells(&self, receiver: &str) -> Result, TellError> { use self::tells::columns; - let conn = &*self.get().expect("Failed to get connection"); - match tells::table + let conn = &*self.get().context(ErrorKind::NoConnection)?; + Ok(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 - } - } + .context(ErrorKind::MysqlError)?) } - fn delete_tells(&mut self, receiver: &str) -> DbResponse { + fn delete_tells(&mut self, receiver: &str) -> Result<(), TellError> { 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") - } - } + let conn = &*self.get().context(ErrorKind::NoConnection)?; + diesel::delete(tells::table.filter(columns::receiver.eq(receiver))) + .execute(conn) + .context(ErrorKind::MysqlError)?; + Ok(()) } } diff --git a/src/plugins/tell/mod.rs b/src/plugins/tell/mod.rs index f781ed8..ccca300 100644 --- a/src/plugins/tell/mod.rs +++ b/src/plugins/tell/mod.rs @@ -9,13 +9,14 @@ use humantime::format_duration; use plugin::*; -use error::FrippyError; -use error::ErrorKind as FrippyErrorKind; use failure::Fail; use failure::ResultExt; +use error::ErrorKind as FrippyErrorKind; +use error::FrippyError; +use self::error::*; pub mod database; -use self::database::{Database, DbResponse}; +use self::database::Database; macro_rules! try_lock { ( $m:expr ) => { @@ -38,16 +39,20 @@ impl Tell { } } - fn tell_command(&self, client: &IrcClient, command: PluginCommand) -> Result<&str, String> { + fn tell_command( + &self, + client: &IrcClient, + command: PluginCommand, + ) -> Result { if command.tokens.len() < 2 { - return Err(self.invalid_command(client)); + return Ok(self.invalid_command(client)); } let receiver = &command.tokens[0]; let sender = command.source; if receiver.eq_ignore_ascii_case(&sender) { - return Err(String::from("That's your name!")); + return Ok(String::from("That's your name!")); } if let Some(channels) = client.list_channels() { @@ -57,7 +62,7 @@ impl Tell { .iter() .any(|u| u.get_nickname().eq_ignore_ascii_case(&receiver)) { - return Err(format!("{} is currently online.", receiver)); + return Ok(format!("{} is currently online.", receiver)); } } } @@ -72,36 +77,49 @@ impl Tell { message: &message, }; - match try_lock!(self.tells).insert_tell(&tell) { - DbResponse::Success => Ok("Got it!"), - DbResponse::Failed(e) => Err(e.to_string()), - } + try_lock!(self.tells).insert_tell(&tell)?; + + Ok(String::from("Got it!")) } fn send_tells(&self, client: &IrcClient, receiver: &str) -> ExecutionStatus { let mut tells = try_lock!(self.tells); - if let Some(tell_messages) = tells.get_tells(&receiver.to_lowercase()) { - for tell in tell_messages { - let now = Duration::new(time::now().to_timespec().sec as u64, 0); - let dur = now - Duration::new(tell.time.timestamp() as u64, 0); - let human_dur = format_duration(dur); - - if let Err(e) = client.send_notice( - receiver, - &format!( - "Tell from {} {} ago: {}", - tell.sender, human_dur, tell.message - ), - ) { - return ExecutionStatus::Err(e.context(FrippyErrorKind::Connection).into()); - } - debug!( - "Sent {:?} from {:?} to {:?}", - tell.message, tell.sender, receiver - ); + + let tell_messages = match tells.get_tells(&receiver.to_lowercase()) { + Ok(t) => t, + Err(e) => { + // This warning only occurs if frippy is built without a database + #[allow(unreachable_patterns)] + return match e.kind() { + ErrorKind::NotFound => ExecutionStatus::Done, + _ => ExecutionStatus::Err(e.context(FrippyErrorKind::Tell).into()), + }; + } + }; + + for tell in tell_messages { + let now = Duration::new(time::now().to_timespec().sec as u64, 0); + let dur = now - Duration::new(tell.time.timestamp() as u64, 0); + let human_dur = format_duration(dur); + + if let Err(e) = client.send_notice( + receiver, + &format!( + "Tell from {} {} ago: {}", + tell.sender, human_dur, tell.message + ), + ) { + return ExecutionStatus::Err(e.context(FrippyErrorKind::Connection).into()); } + debug!( + "Sent {:?} from {:?} to {:?}", + tell.message, tell.sender, receiver + ); } - tells.delete_tells(&receiver.to_lowercase()); + + if let Err(e) = tells.delete_tells(&receiver.to_lowercase()) { + return ExecutionStatus::Err(e.context(FrippyErrorKind::Tell).into()); + }; ExecutionStatus::Done } @@ -126,7 +144,9 @@ impl Tell { impl Plugin for Tell { fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { match message.command { - Command::JOIN(_, _, _) => self.send_tells(client, message.source_nickname().unwrap()), + Command::JOIN(_, _, _) => { + self.send_tells(client, message.source_nickname().unwrap()) + } _ => ExecutionStatus::Done, } } @@ -147,14 +167,16 @@ impl Plugin for Tell { Ok(match command.tokens[0].as_ref() { "help" => client .send_notice(&command.source, &self.help(client)) - .context(FrippyErrorKind::Connection), + .context(FrippyErrorKind::Connection) + .into(), _ => match self.tell_command(client, command) { Ok(msg) => client - .send_notice(&sender, msg) - .context(FrippyErrorKind::Connection), - Err(msg) => client .send_notice(&sender, &msg) .context(FrippyErrorKind::Connection), + Err(e) => client + .send_notice(&sender, &e.to_string()) + .context(FrippyErrorKind::Connection) + .into() }, }?) } @@ -170,3 +192,23 @@ impl fmt::Debug for Tell { write!(f, "Tell {{ ... }}") } } + +pub mod error { + #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail, Error)] + #[error = "TellError"] + pub enum ErrorKind { + /// Not found command error + #[fail(display = "Tell 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, + } +} -- cgit v1.2.3-70-g09d2 From 1bb6e307f1011456b28bb6eb27abc80e71ef187d Mon Sep 17 00:00:00 2001 From: Jokler Date: Mon, 5 Mar 2018 18:11:51 +0100 Subject: Check for outstanding tells when a userlist is received --- src/plugins/tell/database.rs | 17 +++++++++ src/plugins/tell/mod.rs | 87 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 86 insertions(+), 18 deletions(-) (limited to 'src/plugins/tell/database.rs') diff --git a/src/plugins/tell/database.rs b/src/plugins/tell/database.rs index 40ec6fc..98e9fb3 100644 --- a/src/plugins/tell/database.rs +++ b/src/plugins/tell/database.rs @@ -43,6 +43,7 @@ pub struct NewTellMessage<'a> { pub trait Database: Send { fn insert_tell(&mut self, tell: &NewTellMessage) -> Result<(), TellError>; fn get_tells(&self, receiver: &str) -> Result, TellError>; + fn get_receivers(&self) -> Result, TellError>; fn delete_tells(&mut self, receiver: &str) -> Result<(), TellError>; } @@ -69,6 +70,12 @@ impl Database for HashMap> { Ok(self.get(receiver).cloned().ok_or(ErrorKind::NotFound)?) } + fn get_receivers(&self) -> Result, TellError> { + Ok(self.iter() + .map(|(receiver, _)| receiver.to_owned()) + .collect::>()) + } + fn delete_tells(&mut self, receiver: &str) -> Result<(), TellError> { match self.remove(receiver) { Some(_) => Ok(()), @@ -120,6 +127,16 @@ impl Database for Arc>> { .context(ErrorKind::MysqlError)?) } + fn get_receivers(&self) -> Result, TellError> { + use self::tells::columns; + + let conn = &*self.get().context(ErrorKind::NoConnection)?; + Ok(tells::table + .select(columns::receiver) + .load::(conn) + .context(ErrorKind::MysqlError)?) + } + fn delete_tells(&mut self, receiver: &str) -> Result<(), TellError> { use diesel; use self::tells::columns; diff --git a/src/plugins/tell/mod.rs b/src/plugins/tell/mod.rs index dad5235..a5a7116 100644 --- a/src/plugins/tell/mod.rs +++ b/src/plugins/tell/mod.rs @@ -51,6 +51,10 @@ impl Tell { let receiver = &command.tokens[0]; let sender = command.source; + if receiver.eq_ignore_ascii_case(client.current_nickname()) { + return Ok(String::from("I am right here!")); + } + if receiver.eq_ignore_ascii_case(&sender) { return Ok(String::from("That's your name!")); } @@ -82,7 +86,36 @@ impl Tell { Ok(String::from("Got it!")) } - fn send_tells(&self, client: &IrcClient, receiver: &str) -> ExecutionStatus { + fn on_namelist( + &self, + client: &IrcClient, + channel: &str, + ) -> Result<(), FrippyError> { + let receivers = try_lock!(self.tells) + .get_receivers() + .context(FrippyErrorKind::Tell)?; + + if let Some(users) = client.list_users(channel) { + debug!("Outstanding tells for {:?}", receivers); + + for receiver in users + .iter() + .map(|u| u.get_nickname()) + .filter(|u| receivers.iter().any(|r| r == &u.to_lowercase())) + { + self.send_tells(client, receiver)?; + } + + Ok(()) + } else { + Ok(()) + } + } + fn send_tells(&self, client: &IrcClient, receiver: &str) -> Result<(), FrippyError> { + if client.current_nickname() == receiver { + return Ok(()); + } + let mut tells = try_lock!(self.tells); let tell_messages = match tells.get_tells(&receiver.to_lowercase()) { @@ -91,8 +124,8 @@ impl Tell { // This warning only occurs if frippy is built without a database #[allow(unreachable_patterns)] return match e.kind() { - ErrorKind::NotFound => ExecutionStatus::Done, - _ => ExecutionStatus::Err(e.context(FrippyErrorKind::Tell).into()), + ErrorKind::NotFound => Ok(()), + _ => Err(e.context(FrippyErrorKind::Tell))?, }; } }; @@ -102,26 +135,27 @@ impl Tell { let dur = now - Duration::new(tell.time.timestamp() as u64, 0); let human_dur = format_duration(dur); - if let Err(e) = client.send_notice( - receiver, - &format!( - "Tell from {} {} ago: {}", - tell.sender, human_dur, tell.message - ), - ) { - return ExecutionStatus::Err(e.context(FrippyErrorKind::Connection).into()); - } + client + .send_notice( + receiver, + &format!( + "Tell from {} {} ago: {}", + tell.sender, human_dur, tell.message + ), + ) + .context(FrippyErrorKind::Connection)?; + debug!( "Sent {:?} from {:?} to {:?}", tell.message, tell.sender, receiver ); } - if let Err(e) = tells.delete_tells(&receiver.to_lowercase()) { - return ExecutionStatus::Err(e.context(FrippyErrorKind::Tell).into()); - }; + tells + .delete_tells(&receiver.to_lowercase()) + .context(FrippyErrorKind::Tell)?; - ExecutionStatus::Done + Ok(()) } fn invalid_command(&self, client: &IrcClient) -> String { @@ -143,9 +177,26 @@ impl Tell { impl Plugin for Tell { fn execute(&self, client: &IrcClient, message: &Message) -> ExecutionStatus { - match message.command { + let res = match message.command { Command::JOIN(_, _, _) => self.send_tells(client, message.source_nickname().unwrap()), - _ => ExecutionStatus::Done, + Command::Response(resp, ref chan_info, _) => { + if resp == Response::RPL_NAMREPLY { + debug!("NAMREPLY info: {:?}", chan_info); + + self.on_namelist( + client, + &chan_info[chan_info.len() - 1], + ) + } else { + Ok(()) + } + } + _ => Ok(()), + }; + + match res { + Ok(_) => ExecutionStatus::Done, + Err(e) => ExecutionStatus::Err(e), } } -- cgit v1.2.3-70-g09d2