aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins
diff options
context:
space:
mode:
authorJokler <jokler.contact@gmail.com>2018-05-12 21:40:54 +0200
committerJokler <jokler.contact@gmail.com>2018-05-12 21:40:54 +0200
commit90bbc48e8f65566f38ee9bd0b10fbc53ce7ac4a7 (patch)
tree513710d5eb12780c1d50ddd2231cee8da8b0a90d /src/plugins
parent243012bbcdbfae236f37919ddeae232278df8bbc (diff)
downloadfrippy-90bbc48e8f65566f38ee9bd0b10fbc53ce7ac4a7.tar.gz
frippy-90bbc48e8f65566f38ee9bd0b10fbc53ce7ac4a7.zip
Remind: Add initial remind plugin
Mysql is not supported yet.
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/help.rs9
-rw-r--r--src/plugins/mod.rs1
-rw-r--r--src/plugins/remind/database.rs128
-rw-r--r--src/plugins/remind/mod.rs290
-rw-r--r--src/plugins/remind/parser.rs250
5 files changed, 675 insertions, 3 deletions
diff --git a/src/plugins/help.rs b/src/plugins/help.rs
index a02931c..b75086f 100644
--- a/src/plugins/help.rs
+++ b/src/plugins/help.rs
@@ -26,9 +26,12 @@ impl Plugin for Help {
fn command(&self, client: &IrcClient, command: PluginCommand) -> Result<(), FrippyError> {
Ok(client
- .send_notice(&command.source, "Available commands: help, currency, tell, factoids\r\n\
- For more detailed help call help on the specific command.\r\n\
- Example: 'currency help'")
+ .send_notice(
+ &command.source,
+ "Available commands: help, currency, tell, factoids, remind\r\n\
+ For more detailed help call help on the specific command.\r\n\
+ Example: 'currency help'",
+ )
.context(FrippyErrorKind::Connection)?)
}
diff --git a/src/plugins/mod.rs b/src/plugins/mod.rs
index a8fc818..99e35db 100644
--- a/src/plugins/mod.rs
+++ b/src/plugins/mod.rs
@@ -4,6 +4,7 @@ pub mod emoji;
pub mod factoids;
pub mod help;
pub mod keepnick;
+pub mod remind;
pub mod sed;
pub mod tell;
pub mod url;
diff --git a/src/plugins/remind/database.rs b/src/plugins/remind/database.rs
new file mode 100644
index 0000000..c0c127e
--- /dev/null
+++ b/src/plugins/remind/database.rs
@@ -0,0 +1,128 @@
+#[cfg(feature = "mysql")]
+extern crate dotenv;
+
+use std::collections::HashMap;
+use std::collections::hash_map::Entry;
+use std::fmt;
+
+use chrono::NaiveDateTime;
+
+use super::error::*;
+
+#[derive(Clone, Debug)]
+pub struct Event {
+ pub id: i64,
+ pub receiver: String,
+ pub content: String,
+ pub author: String,
+ pub time: NaiveDateTime,
+ pub repeat: Option<u64>,
+}
+
+impl fmt::Display for Event {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}: {} reminds {} to \"{}\" at {}",
+ self.id, self.author, self.receiver, self.content, self.time
+ )
+ }
+}
+
+#[derive(Debug)]
+pub struct NewEvent<'a> {
+ pub receiver: &'a str,
+ pub content: &'a str,
+ pub author: &'a str,
+ pub time: &'a NaiveDateTime,
+ pub repeat: Option<u64>,
+}
+
+pub trait Database: Send + Sync {
+ fn insert_event(&mut self, event: &NewEvent) -> Result<(), RemindError>;
+ fn update_event_time(&mut self, id: i64, &NaiveDateTime) -> Result<(), RemindError>;
+ fn get_events_before(&self, time: &NaiveDateTime) -> Result<Vec<Event>, RemindError>;
+ fn get_user_events(&self, user: &str) -> Result<Vec<Event>, RemindError>;
+ fn get_event(&self, id: i64) -> Result<Event, RemindError>;
+ fn delete_event(&mut self, id: i64) -> Result<(), RemindError>;
+}
+
+// HashMap
+impl Database for HashMap<i64, Event> {
+ fn insert_event(&mut self, event: &NewEvent) -> Result<(), RemindError> {
+ let mut id = 0;
+ while self.contains_key(&id) {
+ id += 1;
+ }
+
+ let event = Event {
+ id: id,
+ receiver: event.receiver.to_owned(),
+ content: event.content.to_owned(),
+ author: event.author.to_owned(),
+ time: event.time.clone(),
+ repeat: event.repeat,
+ };
+
+ match self.insert(id, event) {
+ None => Ok(()),
+ Some(_) => Err(ErrorKind::Duplicate)?,
+ }
+ }
+
+ fn update_event_time(&mut self, id: i64, time: &NaiveDateTime) -> Result<(), RemindError> {
+ let entry = self.entry(id);
+
+ match entry {
+ Entry::Occupied(mut v) => v.get_mut().time = *time,
+ Entry::Vacant(_) => return Err(ErrorKind::NotFound.into()),
+ }
+
+ Ok(())
+ }
+
+ fn get_events_before(&self, time: &NaiveDateTime) -> Result<Vec<Event>, RemindError> {
+ let mut events = Vec::new();
+
+ for (_, event) in self.iter() {
+ if &event.time < time {
+ events.push(event.clone())
+ }
+ }
+
+ if events.is_empty() {
+ Err(ErrorKind::NotFound.into())
+ } else {
+ Ok(events)
+ }
+ }
+
+ fn get_user_events(&self, user: &str) -> Result<Vec<Event>, RemindError> {
+ let mut events = Vec::new();
+
+ for (_, event) in self.iter() {
+ if event.receiver.eq_ignore_ascii_case(user) {
+ events.push(event.clone())
+ }
+ }
+
+ if events.is_empty() {
+ Err(ErrorKind::NotFound.into())
+ } else {
+ Ok(events)
+ }
+ }
+
+ fn get_event(&self, id: i64) -> Result<Event, RemindError> {
+ Ok(self.get(&id)
+ .map(|ev| ev.clone())
+ .ok_or(ErrorKind::NotFound)?)
+ }
+
+ fn delete_event(&mut self, id: i64) -> Result<(), RemindError> {
+ match self.remove(&id) {
+ Some(_) => Ok(()),
+ None => Err(ErrorKind::NotFound)?,
+ }
+ }
+}
diff --git a/src/plugins/remind/mod.rs b/src/plugins/remind/mod.rs
new file mode 100644
index 0000000..0893589
--- /dev/null
+++ b/src/plugins/remind/mod.rs
@@ -0,0 +1,290 @@
+use antidote::RwLock;
+use irc::client::prelude::*;
+use std::thread::{sleep, spawn};
+use std::{fmt, sync::Arc, time::Duration};
+
+use chrono::{self, NaiveDateTime};
+use time;
+
+use plugin::*;
+pub mod database;
+mod parser;
+use self::database::Database;
+use self::parser::CommandParser;
+
+use self::error::*;
+use error::ErrorKind as FrippyErrorKind;
+use error::FrippyError;
+use failure::ResultExt;
+
+fn get_time() -> NaiveDateTime {
+ let tm = time::now().to_timespec();
+ NaiveDateTime::from_timestamp(tm.sec, 0u32)
+}
+
+fn get_events<T: Database>(db: &RwLock<T>, in_next: chrono::Duration) -> Vec<database::Event> {
+ loop {
+ let before = get_time() + in_next;
+ match db.read().get_events_before(&before) {
+ Ok(events) => return events,
+ Err(e) => {
+ if e.kind() != ErrorKind::NotFound {
+ error!("Failed to get events: {}", e);
+ }
+ }
+ }
+
+ debug!("Sleeping for {:?}", in_next);
+ sleep(in_next.to_std().expect("Failed to convert look ahead time"));
+ }
+}
+
+fn run<T: Database>(client: &IrcClient, db: Arc<RwLock<T>>) {
+ let look_ahead = chrono::Duration::minutes(2);
+
+ let mut events = get_events(&db, look_ahead);
+
+ let mut sleep_time = look_ahead
+ .to_std()
+ .expect("Failed to convert look ahead time");
+
+ loop {
+ let now = get_time();
+ for event in events {
+ if event.time <= now {
+ let msg = format!("Reminder from {}: {}", event.author, event.content);
+ if let Err(e) = client.send_notice(&event.receiver, &msg) {
+ error!("Failed to send reminder: {}", e);
+ } else {
+ debug!("Sent reminder {:?}", event);
+
+ if let Some(repeat) = event.repeat {
+ let next_time = event.time + chrono::Duration::seconds(repeat as i64);
+
+ if let Err(e) = db.write().update_event_time(event.id, &next_time) {
+ error!("Failed to update reminder: {}", e);
+ } else {
+ debug!("Updated time on: {:?}", event);
+ }
+ } else if let Err(e) = db.write().delete_event(event.id) {
+ error!("Failed to delete reminder: {}", e);
+ }
+ }
+ } else {
+ let until_event = (event.time - now)
+ .to_std()
+ .expect("Failed to convert until event time");
+
+ if until_event < sleep_time {
+ sleep_time = until_event + Duration::from_secs(1);
+ }
+ }
+ }
+
+ debug!("Sleeping for {:?}", sleep_time);
+ sleep(sleep_time);
+ sleep_time = Duration::from_secs(120);
+
+ events = get_events(&db, look_ahead);
+ }
+}
+
+#[derive(PluginName)]
+pub struct Remind<T: 'static + Database> {
+ events: Arc<RwLock<T>>,
+ has_reminder: RwLock<bool>,
+}
+
+impl<T: 'static + Database> Remind<T> {
+ pub fn new(db: T) -> Self {
+ let events = Arc::new(RwLock::new(db));
+
+ Remind {
+ events: events,
+ has_reminder: RwLock::new(false),
+ }
+ }
+
+ fn set(&self, command: PluginCommand) -> Result<&str, RemindError> {
+ let parser = CommandParser::try_from_tokens(command.tokens)?;
+ debug!("parser: {:?}", parser);
+
+ let mut target = parser.get_target();
+ if target == "me" {
+ target = &command.source;
+ }
+
+ let event = database::NewEvent {
+ receiver: target,
+ content: &parser.get_message(),
+ author: &command.source,
+ time: &parser.get_time(Duration::from_secs(120))?,
+ repeat: parser
+ .get_repeat(Duration::from_secs(300))?
+ .map(|d| d.as_secs()),
+ };
+
+ debug!("New event: {:?}", event);
+
+ Ok(self.events.write().insert_event(&event).map(|()| "Got it")?)
+ }
+
+ fn list(&self, user: &str) -> Result<String, RemindError> {
+ let mut events = self.events.read().get_user_events(user)?;
+
+ let mut list = events.remove(0).to_string();
+ for ev in events {
+ list.push_str("\r\n");
+ list.push_str(&ev.to_string());
+ }
+
+ Ok(list)
+ }
+
+ fn delete(&self, mut command: PluginCommand) -> Result<&str, RemindError> {
+ let id = command
+ .tokens
+ .remove(0)
+ .parse::<i64>()
+ .context(ErrorKind::Parsing)?;
+ let event = self.events.read().get_event(id)?;
+
+ if event.receiver.eq_ignore_ascii_case(&command.source)
+ || event.author.eq_ignore_ascii_case(&command.source)
+ {
+ self.events
+ .write()
+ .delete_event(id)
+ .map(|()| "Successfully deleted")
+ } else {
+ Ok("Only the author or receiver can delete a reminder")
+ }
+ }
+
+ fn help(&self) -> &str {
+ "usage: remind <subcommand>\r\n\
+ subcommands: new, list, delete, help"
+ }
+}
+
+impl<T: Database> Plugin for Remind<T> {
+ fn execute(&self, client: &IrcClient, _: &Message) -> ExecutionStatus {
+ let mut has_reminder = self.has_reminder.write();
+ if !*has_reminder {
+ let events = Arc::clone(&self.events);
+ let client = client.clone();
+
+ spawn(move || run(&client, events));
+
+ *has_reminder = true;
+ }
+
+ ExecutionStatus::Done
+ }
+
+ fn execute_threaded(&self, _: &IrcClient, _: &Message) -> Result<(), FrippyError> {
+ panic!("Remind should not use frippy's threading")
+ }
+
+ fn command(&self, client: &IrcClient, mut command: PluginCommand) -> Result<(), FrippyError> {
+ if command.tokens.is_empty() {
+ return Ok(client
+ .send_notice(&command.source, &ErrorKind::InvalidCommand.to_string())
+ .context(FrippyErrorKind::Connection)?);
+ }
+
+ let source = command.source.clone();
+
+ let sub_command = command.tokens.remove(0);
+ let response = match sub_command.as_ref() {
+ "new" => self.set(command).map(|s| s.to_owned()),
+ "delete" => self.delete(command).map(|s| s.to_owned()),
+ "list" => self.list(&source),
+ "help" => Ok(self.help().to_owned()),
+ _ => Err(ErrorKind::InvalidCommand.into()),
+ };
+
+ let result = match response {
+ Ok(msg) => client
+ .send_notice(&source, &msg)
+ .context(FrippyErrorKind::Connection)?,
+ Err(e) => {
+ let message = e.to_string();
+
+ client
+ .send_notice(&source, &message)
+ .context(FrippyErrorKind::Connection)?;
+
+ Err(e).context(FrippyErrorKind::Remind)?
+ }
+ };
+
+ Ok(result)
+ }
+
+ fn evaluate(&self, _: &IrcClient, _: PluginCommand) -> Result<String, String> {
+ Err(String::from(
+ "Evaluation of commands is not implemented for remind at this time",
+ ))
+ }
+}
+
+impl<T: Database> fmt::Debug for Remind<T> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Remind {{ ... }}")
+ }
+}
+
+pub mod error {
+ #[derive(Copy, Clone, Eq, PartialEq, Debug, Fail, Error)]
+ #[error = "RemindError"]
+ pub enum ErrorKind {
+ /// Invalid command error
+ #[fail(display = "Incorrect Command. Send \"currency help\" for help.")]
+ InvalidCommand,
+
+ /// Missing message error
+ #[fail(display = "Reminder needs to have a description")]
+ MissingMessage,
+
+ /// Missing receiver error
+ #[fail(display = "Specify who to remind")]
+ MissingReceiver,
+
+ /// Missing time error
+ #[fail(display = "Reminder needs to have a time")]
+ MissingTime,
+
+ /// Invalid time error
+ #[fail(display = "Could not parse time")]
+ InvalidTime,
+
+ /// Invalid date error
+ #[fail(display = "Could not parse date")]
+ InvalidDate,
+
+ /// Parse error
+ #[fail(display = "Could not parse integers")]
+ Parsing,
+
+ /// Ambigous time error
+ #[fail(display = "Time specified is ambiguous")]
+ AmbiguousTime,
+
+ /// Time too short error
+ #[fail(display = "Reminder needs to be in over 2 minutes")]
+ TimeShort,
+
+ /// Repeat time too short error
+ #[fail(display = "Repeat time needs to be over 5 minutes")]
+ RepeatTimeShort,
+
+ /// Duplicate error
+ #[fail(display = "Entry already exists")]
+ Duplicate,
+
+ /// Not found error
+ #[fail(display = "No events found")]
+ NotFound,
+ }
+}
diff --git a/src/plugins/remind/parser.rs b/src/plugins/remind/parser.rs
new file mode 100644
index 0000000..2dbb040
--- /dev/null
+++ b/src/plugins/remind/parser.rs
@@ -0,0 +1,250 @@
+use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
+use humantime::parse_duration;
+use std::time::Duration;
+use time;
+
+use super::error::*;
+use failure::ResultExt;
+
+#[derive(Default, Debug)]
+pub struct CommandParser {
+ on_date: Option<String>,
+ at_time: Option<String>,
+ in_duration: Option<String>,
+ every_time: Option<String>,
+ target: String,
+ message: Option<String>,
+}
+
+#[derive(PartialEq, Clone, Copy)]
+enum ParseState {
+ None,
+ On,
+ At,
+ In,
+ Every,
+ Msg,
+}
+
+impl CommandParser {
+ pub fn try_from_tokens(tokens: Vec<String>) -> Result<Self, RemindError> {
+ if tokens.is_empty() {
+ return Err(ErrorKind::MissingReceiver.into());
+ }
+
+ let mut parser = CommandParser::default();
+ let mut state = ParseState::None;
+
+ let mut iter = tokens.into_iter();
+ parser.target = iter.next()
+ .expect("This should be guaranteed by the length check");
+
+ let mut cur_str = String::new();
+ while let Some(token) = iter.next() {
+ let next_state = match token.as_ref() {
+ "on" => ParseState::On,
+ "at" => ParseState::At,
+ "in" => ParseState::In,
+ "every" => ParseState::Every,
+ "to" => ParseState::Msg,
+ _ => {
+ if !cur_str.is_empty() {
+ cur_str.push(' ');
+ }
+ cur_str.push_str(&token);
+ state
+ }
+ };
+
+ if next_state != state {
+ if state != ParseState::None {
+ parser = parser.add_string_by_state(&state, cur_str)?;
+ cur_str = String::new();
+ }
+
+ state = next_state;
+ }
+ }
+ parser = parser.add_string_by_state(&state, cur_str)?;
+
+ if parser.message.is_none() {
+ return Err(ErrorKind::MissingMessage.into());
+ }
+
+ if parser.in_duration.is_some() && parser.at_time.is_some()
+ || parser.in_duration.is_some() && parser.on_date.is_some()
+ {
+ return Err(ErrorKind::AmbiguousTime.into());
+ }
+
+ if parser.in_duration.is_none() && parser.at_time.is_none() && parser.on_date.is_none() {
+ return Err(ErrorKind::MissingTime.into());
+ }
+
+ Ok(parser)
+ }
+
+ fn add_string_by_state(self, state: &ParseState, string: String) -> Result<Self, RemindError> {
+ use self::ParseState::*;
+ let string = Some(string);
+ match state {
+ &On if self.on_date.is_none() => {
+ return Ok(CommandParser {
+ on_date: string,
+ ..self
+ })
+ }
+ &At if self.at_time.is_none() => {
+ return Ok(CommandParser {
+ at_time: string,
+ ..self
+ })
+ }
+ &In if self.in_duration.is_none() => {
+ return Ok(CommandParser {
+ in_duration: string,
+ ..self
+ })
+ }
+ &Msg if self.message.is_none() => {
+ return Ok(CommandParser {
+ message: string,
+ ..self
+ })
+ }
+ &Every if self.every_time.is_none() => {
+ return Ok(CommandParser {
+ every_time: string,
+ ..self
+ })
+ }
+ _ => Err(ErrorKind::MissingMessage.into()),
+ }
+ }
+
+ fn parse_date(&self, str_date: &str) -> Result<NaiveDate, RemindError> {
+ let nums = str_date
+ .split('.')
+ .map(|s| s.parse::<u32>())
+ .collect::<Result<Vec<_>, _>>()
+ .context(ErrorKind::InvalidDate)?;
+
+ if 2 > nums.len() || nums.len() > 3 {
+ return Err(ErrorKind::InvalidDate.into());
+ }
+
+ let day = nums[0];
+ let month = nums[1];
+
+ let parse_date = match nums.get(2) {
+ Some(year) => {
+ NaiveDate::from_ymd_opt(*year as i32, month, day).ok_or(ErrorKind::InvalidDate)?
+ }
+ None => {
+ let now = time::now();
+ let date = NaiveDate::from_ymd_opt(now.tm_year + 1900, month, day)
+ .ok_or(ErrorKind::InvalidDate)?;
+ if date.succ().and_hms(0, 0, 0).timestamp() < now.to_timespec().sec {
+ NaiveDate::from_ymd(now.tm_year + 1901, month, day)
+ } else {
+ date
+ }
+ }
+ };
+
+ Ok(parse_date)
+ }
+
+ fn parse_time(&self, str_time: &str) -> Result<NaiveTime, RemindError> {
+ let nums = str_time
+ .split(':')
+ .map(|s| s.parse::<u32>())
+ .collect::<Result<Vec<_>, _>>()
+ .context(ErrorKind::InvalidTime)?;
+
+ if 2 != nums.len() {
+ return Err(ErrorKind::InvalidTime.into());
+ }
+
+ let hour = nums[0];
+ let minute = nums[1];
+
+ Ok(NaiveTime::from_hms(hour, minute, 0))
+ }
+
+ pub fn get_time(&self, min_dur: Duration) -> Result<NaiveDateTime, RemindError> {
+ if let Some(ref str_duration) = self.in_duration {
+ let duration = parse_duration(&str_duration).context(ErrorKind::InvalidTime)?;
+
+ if duration < min_dur {
+ return Err(ErrorKind::TimeShort.into());
+ }
+
+ let tm = time::now().to_timespec();
+ return Ok(NaiveDateTime::from_timestamp(
+ tm.sec + duration.as_secs() as i64,
+ 0u32,
+ ));
+ }
+
+ let mut date = None;
+ if let Some(ref str_date) = self.on_date {
+ date = Some(self.parse_date(str_date)?);
+ }
+
+ if let Some(ref str_time) = self.at_time {
+ let time = self.parse_time(str_time)?;
+
+ if let Some(date) = date {
+ Ok(date.and_time(time))
+ } else {
+ let now = time::now();
+ let today = NaiveDate::from_ymd_opt(
+ now.tm_year + 1900,
+ now.tm_mon as u32 + 1,
+ now.tm_mday as u32,
+ ).ok_or(ErrorKind::InvalidDate)?;
+
+ let time_today = today.and_time(time);
+
+ if time_today.timestamp() < now.to_timespec().sec {
+ debug!("tomorrow");
+
+ Ok(today.succ().and_time(time))
+ } else {
+ debug!("today");
+
+ Ok(time_today)
+ }
+ }
+ } else {
+ Ok(date.expect("At this point date has to be set")
+ .and_hms(0, 0, 0))
+ }
+ }
+
+ pub fn get_repeat(&self, min_dur: Duration) -> Result<Option<Duration>, RemindError> {
+ if let Some(mut words) = self.every_time.clone() {
+ if !words.chars().next().unwrap().is_digit(10) {
+ words.insert(0, '1');
+ }
+ let dur = parse_duration(&words).context(ErrorKind::InvalidTime)?;
+
+ if dur < min_dur {
+ return Err(ErrorKind::RepeatTimeShort.into());
+ }
+
+ Ok(Some(dur))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub fn get_target(&self) -> &str {
+ &self.target
+ }
+
+ pub fn get_message(&self) -> &str {
+ self.message.as_ref().expect("Has to be set")
+ }
+}