This repository has been archived by the owner. It is now read-only.
Permalink
Browse files
Add features and fix some bugs in remind.coffee
- Adds support for "remind me on <date/time> to <action>" - Requires new npm packages: - moment, for duration/time formatting - lodash, for utility functions - chrono-node, for parsing natural language type dates (e.g. next Tuesday, Tomorrow at 3:00pm, etc.)
@@ -1,99 +1,157 @@ | ||
# Description: | ||
# Forgetful? Add reminders | ||
# Forgetful? Add reminders! | ||
# | ||
# Dependencies: | ||
# None | ||
# "chrono-node": "^0.1.10" | ||
# "moment": "^2.8.1" | ||
# "lodash": "^2.4.1" | ||
# | ||
# Configuration: | ||
# None | ||
# | ||
# Commands: | ||
# hubot remind me in <time> to <action> - Set a reminder in <time> to do an <action> <time> is in the format 1 day, 2 hours, 5 minutes etc. Time segments are optional, as are commas | ||
# hubot remind me (on <date>|in <time>) to <action> - Set a reminder in <time> to do an <action> <time> is in the format 1 day, 2 hours, 5 minutes etc. Time segments are optional, as are commas | ||
# hubot delete reminder <action> - Delete reminder matching <action> (exact match required) | ||
# hubot show reminders | ||
# | ||
# Author: | ||
# whitman | ||
# jtwalters | ||
|
||
_ = require('lodash') | ||
moment = require('moment') | ||
chrono = require('chrono-node') | ||
timeoutIds = {} | ||
|
||
class Reminders | ||
constructor: (@robot) -> | ||
@cache = [] | ||
@current_timeout = null | ||
@currentTimeout = null | ||
|
||
@robot.brain.on 'loaded', => | ||
# Load reminders from brain, on loaded event | ||
@robot.brain.on('loaded', => | ||
if @robot.brain.data.reminders | ||
@cache = @robot.brain.data.reminders | ||
@cache = _.map(@robot.brain.data.reminders, (item) -> | ||
new Reminder(item) | ||
) | ||
console.log("loaded #{@cache.length} reminders") | ||
@queue() | ||
) | ||
|
||
# Persist reminders to the brain, on save event | ||
@robot.brain.on('save', => | ||
@robot.brain.data.reminders = @cache | ||
) | ||
|
||
add: (reminder) -> | ||
@cache.push reminder | ||
@cache.sort (a, b) -> a.due - b.due | ||
@robot.brain.data.reminders = @cache | ||
@cache.push(reminder) | ||
@cache.sort((a, b) -> a.due - b.due) | ||
@queue() | ||
|
||
removeFirst: -> | ||
reminder = @cache.shift() | ||
@robot.brain.data.reminders = @cache | ||
return reminder | ||
|
||
queue: -> | ||
clearTimeout @current_timeout if @current_timeout | ||
if @cache.length > 0 | ||
now = new Date().getTime() | ||
@removeFirst() until @cache.length is 0 or @cache[0].due > now | ||
if @cache.length > 0 | ||
trigger = => | ||
reminder = @removeFirst() | ||
@robot.reply reminder.msg_envelope, 'you asked me to remind you to ' + reminder.action | ||
@queue() | ||
# setTimeout uses a 32-bit INT | ||
extendTimeout = (timeout, callback) -> | ||
if timeout > 0x7FFFFFFF | ||
@current_timeout = setTimeout -> | ||
extendTimeout (timeout - 0x7FFFFFFF), callback | ||
, 0x7FFFFFFF | ||
else | ||
@current_timeout = setTimeout callback, timeout | ||
extendTimeout @cache[0].due - now, trigger | ||
return if @cache.length is 0 | ||
now = (new Date).getTime() | ||
trigger = => | ||
reminder = @removeFirst() | ||
@robot.reply(reminder.msg_envelope, 'you asked me to remind you to ' + reminder.action) | ||
@queue() | ||
# setTimeout uses a 32-bit INT | ||
extendTimeout = (timeout, callback) -> | ||
if timeout > 0x7FFFFFFF | ||
setTimeout(-> | ||
extendTimeout(timeout - 0x7FFFFFFF, callback) | ||
, 0x7FFFFFFF) | ||
else | ||
setTimeout(callback, timeout) | ||
reminder = @cache[0] | ||
duration = reminder.due - now | ||
duration = 0 if duration < 0 | ||
clearTimeout(timeoutIds[reminder]) | ||
timeoutIds[reminder] = extendTimeout(reminder.due - now, trigger) | ||
console.log("reminder set with duration of #{duration}") | ||
|
||
class Reminder | ||
constructor: (@msg_envelope, @time, @action) -> | ||
@time.replace(/^\s+|\s+$/g, '') | ||
|
||
periods = | ||
weeks: | ||
value: 0 | ||
regex: "weeks?" | ||
days: | ||
value: 0 | ||
regex: "days?" | ||
hours: | ||
value: 0 | ||
regex: "hours?|hrs?" | ||
minutes: | ||
value: 0 | ||
regex: "minutes?|mins?" | ||
seconds: | ||
value: 0 | ||
regex: "seconds?|secs?" | ||
|
||
for period of periods | ||
pattern = new RegExp('^.*?([\\d\\.]+)\\s*(?:(?:' + periods[period].regex + ')).*$', 'i') | ||
matches = pattern.exec(@time) | ||
periods[period].value = parseInt(matches[1]) if matches | ||
|
||
@due = new Date().getTime() | ||
@due += ((periods.weeks.value * 604800) + (periods.days.value * 86400) + (periods.hours.value * 3600) + (periods.minutes.value * 60) + periods.seconds.value) * 1000 | ||
|
||
dueDate: -> | ||
dueDate = new Date @due | ||
dueDate.toLocaleString() | ||
constructor: (data) -> | ||
{@msg_envelope, @action, @time, @due} = data | ||
|
||
if @time and !@due | ||
@time.replace(/^\s+|\s+$/g, '') | ||
|
||
periods = | ||
weeks: | ||
value: 0 | ||
regex: "weeks?" | ||
days: | ||
value: 0 | ||
regex: "days?" | ||
hours: | ||
value: 0 | ||
regex: "hours?|hrs?" | ||
minutes: | ||
value: 0 | ||
regex: "minutes?|mins?" | ||
seconds: | ||
value: 0 | ||
regex: "seconds?|secs?" | ||
|
||
for period of periods | ||
pattern = new RegExp('^.*?([\\d\\.]+)\\s*(?:(?:' + periods[period].regex + ')).*$', 'i') | ||
matches = pattern.exec(@time) | ||
periods[period].value = parseInt(matches[1]) if matches | ||
|
||
@due = (new Date).getTime() | ||
@due += ( | ||
(periods.weeks.value * 604800) + | ||
(periods.days.value * 86400) + | ||
(periods.hours.value * 3600) + | ||
(periods.minutes.value * 60) + | ||
periods.seconds.value | ||
) * 1000 | ||
|
||
formatDue: -> | ||
dueDate = new Date(@due) | ||
duration = dueDate - new Date | ||
if duration > 0 and duration < 86400000 | ||
'in ' + moment.duration(duration).humanize() | ||
else | ||
'on ' + moment(dueDate).format("dddd, MMMM Do YYYY, h:mm:ss a") | ||
|
||
module.exports = (robot) -> | ||
reminders = new Reminders(robot) | ||
|
||
robot.respond(/show reminders$/i, (msg) -> | ||
text = '' | ||
for reminder in reminders.cache | ||
text += "#{reminder.action} #{reminder.formatDue()}\n" | ||
msg.send(text) | ||
) | ||
|
||
reminders = new Reminders robot | ||
robot.respond(/delete reminder (.+)$/i, (msg) -> | ||
query = msg.match[1] | ||
prevLength = reminders.cache.length | ||
reminders.cache = _.reject(reminders.cache, {action: query}) | ||
reminders.queue() | ||
msg.send("Deleted reminder #{query}") if reminders.cache.length isnt prevLength | ||
) | ||
|
||
robot.respond /remind me in ((?:(?:\d+) (?:weeks?|days?|hours?|hrs?|minutes?|mins?|seconds?|secs?)[ ,]*(?:and)? +)+)to (.*)/i, (msg) -> | ||
time = msg.match[1] | ||
action = msg.match[2] | ||
reminder = new Reminder msg.envelope, time, action | ||
reminders.add reminder | ||
msg.send 'I\'ll remind you to ' + action + ' on ' + reminder.dueDate() | ||
robot.respond(/remind me (in|on) (.+?) to (.*)/i, (msg) -> | ||
type = msg.match[1] | ||
time = msg.match[2] | ||
action = msg.match[3] | ||
options = | ||
msg_envelope: msg.envelope, | ||
action: action | ||
time: time | ||
if type is 'on' | ||
# parse the date (convert to timestamp) | ||
due = chrono.parseDate(time).getTime() | ||
if due.toString() isnt 'Invalid Date' | ||
options.due = due | ||
reminder = new Reminder(options) | ||
reminders.add(reminder) | ||
msg.send "I'll remind you to #{action} #{reminder.formatDue()}" | ||
) |