aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/remind
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/remind')
-rw-r--r--src/plugins/remind/database.rs236
-rw-r--r--src/plugins/remind/mod.rs337
-rw-r--r--src/plugins/remind/parser.rs251
3 files changed, 824 insertions, 0 deletions
diff --git a/src/plugins/remind/database.rs b/src/plugins/remind/database.rs
new file mode 100644
index 0000000..97d93e8
--- /dev/null
+++ b/src/plugins/remind/database.rs
@@ -0,0 +1,236 @@
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+use std::fmt;
+
+#[cfg(feature = "mysql")]
+use std::sync::Arc;
+
+#[cfg(feature = "mysql")]
+use diesel::mysql::MysqlConnection;
+#[cfg(feature = "mysql")]
+use diesel::prelude::*;
+#[cfg(feature = "mysql")]
+use r2d2::Pool;
+#[cfg(feature = "mysql")]
+use r2d2_diesel::ConnectionManager;
+
+#[cfg(feature = "mysql")]
+use failure::ResultExt;
+
+use chrono::NaiveDateTime;
+
+use super::error::*;
+
+#[cfg(feature = "mysql")]
+static LAST_ID_SQL: &'static str = "SELECT LAST_INSERT_ID()";
+
+#[cfg_attr(feature = "mysql", derive(Queryable))]
+#[derive(Clone, Debug)]
+pub struct Event {
+ pub id: i64,
+ pub receiver: String,
+ pub content: String,
+ pub author: String,
+ pub time: NaiveDateTime,
+ pub repeat: Option<i64>,
+}
+
+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
+ )
+ }
+}
+
+#[cfg_attr(feature = "mysql", derive(Insertable))]
+#[cfg_attr(feature = "mysql", table_name = "events")]
+#[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<i64>,
+}
+
+pub trait Database: Send + Sync {
+ fn insert_event(&mut self, event: &NewEvent) -> Result<i64, RemindError>;
+ fn update_event_time(&mut self, id: i64, time: &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<S: ::std::hash::BuildHasher + Send + Sync> Database for HashMap<i64, Event, S> {
+ fn insert_event(&mut self, event: &NewEvent) -> Result<i64, RemindError> {
+ let mut id = 0;
+ while self.contains_key(&id) {
+ id += 1;
+ }
+
+ let event = Event {
+ id,
+ receiver: event.receiver.to_owned(),
+ content: event.content.to_owned(),
+ author: event.author.to_owned(),
+ time: *event.time,
+ repeat: event.repeat,
+ };
+
+ match self.insert(id, event) {
+ None => Ok(id),
+ 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).cloned().ok_or(ErrorKind::NotFound)?)
+ }
+
+ fn delete_event(&mut self, id: i64) -> Result<(), RemindError> {
+ match self.remove(&id) {
+ Some(_) => Ok(()),
+ None => Err(ErrorKind::NotFound)?,
+ }
+ }
+}
+
+#[cfg(feature = "mysql")]
+mod schema {
+ table! {
+ events (id) {
+ id -> Bigint,
+ receiver -> Varchar,
+ content -> Text,
+ author -> Varchar,
+ time -> Timestamp,
+ repeat -> Nullable<Bigint>,
+ }
+ }
+}
+
+#[cfg(feature = "mysql")]
+use self::schema::events;
+
+#[cfg(feature = "mysql")]
+impl Database for Arc<Pool<ConnectionManager<MysqlConnection>>> {
+ fn insert_event(&mut self, event: &NewEvent) -> Result<i64, RemindError> {
+ use diesel::{self, dsl::sql, types::Bigint};
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+
+ diesel::insert_into(events::table)
+ .values(event)
+ .execute(conn)
+ .context(ErrorKind::MysqlError)?;
+
+ let id = sql::<Bigint>(LAST_ID_SQL)
+ .get_result(conn)
+ .context(ErrorKind::MysqlError)?;
+
+ Ok(id)
+ }
+
+ fn update_event_time(&mut self, id: i64, time: &NaiveDateTime) -> Result<(), RemindError> {
+ use self::events::columns;
+ use diesel;
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+
+ match diesel::update(events::table.filter(columns::id.eq(id)))
+ .set(columns::time.eq(time))
+ .execute(conn)
+ {
+ Ok(0) => Err(ErrorKind::NotFound)?,
+ Ok(_) => Ok(()),
+ Err(e) => Err(e).context(ErrorKind::MysqlError)?,
+ }
+ }
+
+ fn get_events_before(&self, time: &NaiveDateTime) -> Result<Vec<Event>, RemindError> {
+ use self::events::columns;
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+
+ Ok(events::table
+ .filter(columns::time.lt(time))
+ .load::<Event>(conn)
+ .context(ErrorKind::MysqlError)?)
+ }
+
+ fn get_user_events(&self, user: &str) -> Result<Vec<Event>, RemindError> {
+ use self::events::columns;
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+
+ Ok(events::table
+ .filter(columns::receiver.eq(user))
+ .load::<Event>(conn)
+ .context(ErrorKind::MysqlError)?)
+ }
+
+ fn get_event(&self, id: i64) -> Result<Event, RemindError> {
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+
+ Ok(events::table
+ .find(id)
+ .first(conn)
+ .context(ErrorKind::MysqlError)?)
+ }
+
+ fn delete_event(&mut self, id: i64) -> Result<(), RemindError> {
+ use self::events::columns;
+ use diesel;
+
+ let conn = &*self.get().context(ErrorKind::NoConnection)?;
+ match diesel::delete(events::table.filter(columns::id.eq(id))).execute(conn) {
+ Ok(0) => Err(ErrorKind::NotFound)?,
+ Ok(_) => Ok(()),
+ Err(e) => Err(e).context(ErrorKind::MysqlError)?,
+ }
+ }
+}
diff --git a/src/plugins/remind/mod.rs b/src/plugins/remind/mod.rs
new file mode 100644
index 0000000..2a8a093
--- /dev/null
+++ b/src/plugins/remind/mod.rs
@@ -0,0 +1,337 @@
+use std::marker::PhantomData;
+use std::thread::{sleep, spawn};
+use std::{fmt, sync::Arc, time::Duration};
+
+use antidote::RwLock;
+use irc::client::prelude::*;
+
+use chrono::{self, NaiveDateTime};
+use time;
+
+use plugin::*;
+use FrippyClient;
+
+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);
+ }
+ }
+ }
+
+ sleep(in_next.to_std().expect("Failed to convert look ahead time"));
+ }
+}
+
+fn run<T: Database, C: FrippyClient>(client: &C, 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);
+
+ if let Err(e) = db.write().update_event_time(event.id, &next_time) {
+ error!("Failed to update reminder: {}", e);
+ } else {
+ debug!("Updated time");
+ }
+ } 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);
+ }
+ }
+ }
+
+ sleep(sleep_time);
+ sleep_time = Duration::from_secs(120);
+
+ events = get_events(&db, look_ahead);
+ }
+}
+
+#[derive(PluginName)]
+pub struct Remind<T: Database + 'static, C> {
+ events: Arc<RwLock<T>>,
+ has_reminder: RwLock<bool>,
+ phantom: PhantomData<C>,
+}
+
+impl<T: Database + 'static, C: FrippyClient> Remind<T, C> {
+ pub fn new(db: T) -> Self {
+ let events = Arc::new(RwLock::new(db));
+
+ Remind {
+ events,
+ has_reminder: RwLock::new(false),
+ phantom: PhantomData,
+ }
+ }
+
+ fn user_cmd(&self, command: PluginCommand) -> Result<String, RemindError> {
+ let parser = CommandParser::parse_target(command.tokens)?;
+
+ self.set(&parser, &command.source)
+ }
+
+ fn me_cmd(&self, command: PluginCommand) -> Result<String, RemindError> {
+ let source = command.source.clone();
+ let parser = CommandParser::with_target(command.tokens, command.source)?;
+
+ self.set(&parser, &source)
+ }
+
+ fn set(&self, parser: &CommandParser, author: &str) -> Result<String, RemindError> {
+ debug!("parser: {:?}", parser);
+
+ let target = parser.get_target();
+ let time = parser.get_time(Duration::from_secs(120))?;
+
+ let event = database::NewEvent {
+ receiver: target,
+ content: &parser.get_message(),
+ author,
+ time: &time,
+ repeat: parser
+ .get_repeat(Duration::from_secs(300))?
+ .map(|d| d.as_secs() as i64),
+ };
+
+ debug!("New event: {:?}", event);
+
+ Ok(self.events
+ .write()
+ .insert_event(&event)
+ .map(|id| format!("Created reminder with id {} at {} UTC", id, time))?)
+ }
+
+ fn list(&self, user: &str) -> Result<String, RemindError> {
+ let mut events = self.events.read().get_user_events(user)?;
+
+ if events.is_empty() {
+ Err(ErrorKind::NotFound)?;
+ }
+
+ 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)
+ .context(ErrorKind::NotFound)?;
+
+ 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: user, me, list, delete, help\r\n\
+ examples\r\n\
+ remind user foo to sleep in 1 hour\r\n\
+ remind me to leave early on 1.1 at 16:00 every week"
+ }
+}
+
+impl<T: Database, C: FrippyClient + 'static> Plugin for Remind<T, C> {
+ type Client = C;
+ fn execute(&self, client: &Self::Client, msg: &Message) -> ExecutionStatus {
+ if let Command::JOIN(_, _, _) = msg.command {
+ 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, _: &Self::Client, _: &Message) -> Result<(), FrippyError> {
+ panic!("Remind should not use frippy's threading")
+ }
+
+ fn command(
+ &self,
+ client: &Self::Client,
+ mut command: PluginCommand,
+ ) -> Result<(), FrippyError> {
+ if command.tokens.is_empty() {
+ client
+ .send_notice(&command.source, &ErrorKind::InvalidCommand.to_string())
+ .context(FrippyErrorKind::Connection)?;
+ return Ok(());
+ }
+
+ let source = command.source.clone();
+
+ let sub_command = command.tokens.remove(0);
+ let response = match sub_command.as_ref() {
+ "user" => self.user_cmd(command),
+ "me" => self.me_cmd(command),
+ "delete" => self.delete(command).map(|s| s.to_owned()),
+ "list" => self.list(&source),
+ "help" => Ok(self.help().to_owned()),
+ _ => Err(ErrorKind::InvalidCommand.into()),
+ };
+
+ 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(())
+ }
+
+ fn evaluate(&self, _: &Self::Client, _: PluginCommand) -> Result<String, String> {
+ Err(String::from(
+ "Evaluation of commands is not implemented for remind at this time",
+ ))
+ }
+}
+
+impl<T: Database, C: FrippyClient> fmt::Debug for Remind<T, C> {
+ 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 \"remind 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,
+
+ /// 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,
+ }
+}
diff --git a/src/plugins/remind/parser.rs b/src/plugins/remind/parser.rs
new file mode 100644
index 0000000..91d13ab
--- /dev/null
+++ b/src/plugins/remind/parser.rs
@@ -0,0 +1,251 @@
+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 parse_target(mut tokens: Vec<String>) -> Result<Self, RemindError> {
+ let mut parser = CommandParser::default();
+
+ if tokens.is_empty() {
+ Err(ErrorKind::MissingReceiver)?;
+ }
+
+ parser.target = tokens.remove(0);
+
+ parser.parse_tokens(tokens)
+ }
+
+ pub fn with_target(tokens: Vec<String>, target: String) -> Result<Self, RemindError> {
+ let mut parser = CommandParser::default();
+ parser.target = target;
+
+ parser.parse_tokens(tokens)
+ }
+
+ fn parse_tokens(mut self, tokens: Vec<String>) -> Result<Self, RemindError> {
+ let mut state = ParseState::None;
+ let mut cur_str = String::new();
+
+ for token in tokens {
+ 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 {
+ self = self.add_string_by_state(state, cur_str)?;
+ cur_str = String::new();
+ }
+
+ state = next_state;
+ }
+ }
+
+ self = self.add_string_by_state(state, cur_str)?;
+
+ if self.message.is_none() {
+ return Err(ErrorKind::MissingMessage.into());
+ }
+
+ if self.in_duration.is_some() && self.at_time.is_some()
+ || self.in_duration.is_some() && self.on_date.is_some()
+ {
+ return Err(ErrorKind::AmbiguousTime.into());
+ }
+
+ if self.in_duration.is_none() && self.at_time.is_none() && self.on_date.is_none() {
+ return Err(ErrorKind::MissingTime.into());
+ }
+
+ Ok(self)
+ }
+
+ 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() => Ok(CommandParser {
+ on_date: string,
+ ..self
+ }),
+ At if self.at_time.is_none() => Ok(CommandParser {
+ at_time: string,
+ ..self
+ }),
+ In if self.in_duration.is_none() => Ok(CommandParser {
+ in_duration: string,
+ ..self
+ }),
+ Msg if self.message.is_none() => Ok(CommandParser {
+ message: string,
+ ..self
+ }),
+ Every if self.every_time.is_none() => 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")
+ }
+}