This repository has been archived by the owner on Jun 8, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.)
- Loading branch information
Showing
1 changed file
with
124 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()}" | ||
) |