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()}" | ||
| ) |