From eafc14c20f82f799616257d8ebdcd0ca9f955c3f Mon Sep 17 00:00:00 2001
From: Shaun Burdick <github@shaunburdick.com>
Date: Tue, 16 Jun 2015 17:20:09 -0400
Subject: [PATCH] Added some bug fixes and resolves #2

---
 package.json                     |   4 +-
 release/js/lib/ooo_user.js       | 178 ++++++++++++++++++++---
 release/js/spec/ooo_user.spec.js | 123 ++++++++++++++--
 src/lib/ooo_user.ts              | 238 ++++++++++++++++++++++++++-----
 src/spec/ooo_user.spec.ts        | 144 +++++++++++++++++--
 5 files changed, 606 insertions(+), 81 deletions(-)

diff --git a/package.json b/package.json
index 090befd..c5c9856 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
   "author": "Shaun Burdick <github@shaunburdick.com>",
   "license": "MIT",
   "dependencies": {
+    "chrono-node": "^1.0.6",
+    "moment": "^2.10.3",
     "slack-client": "^1.4.1",
     "winston": "^1.0.0"
   },
@@ -19,6 +21,6 @@
     "gulp-batch": "^1.0.5",
     "gulp-typescript": "^2.7.6",
     "gulp-watch": "^4.2.4",
-    "jasmine-node": "^1.14.5"
+    "jasmine-node": "^2.0.0"
   }
 }
diff --git a/release/js/lib/ooo_user.js b/release/js/lib/ooo_user.js
index 431e7b6..ac76b6b 100644
--- a/release/js/lib/ooo_user.js
+++ b/release/js/lib/ooo_user.js
@@ -1,5 +1,7 @@
 /// <reference path="../typings/tsd.d.ts" />
 var logger = require('./logger');
+var moment = require('moment');
+var chrono = require('chrono-node');
 var OOO_User = (function () {
     /**
      * Constructor
@@ -13,12 +15,15 @@ var OOO_User = (function () {
         this.STATUS_AWAITING_MESSAGE = 'awaiting_message';
         this.STATUS_REGISTERED = 'registered';
         this.STATUS_AWAITING_DEREGISTER = 'awaiting_deregister';
+        this.COMMAND_MESSAGE = 'message';
+        this.COMMAND_START = 'start';
+        this.COMMAND_END = 'end';
         this.status = this.STATUS_UNCONFIRMED;
         this.MESSAGE_TIMEOUT = 60000; // Five minutes
         this.DEREGISTER_TIMEOUT = 60000; // Five minutes
         this.POSITIVE_REGEXP = /(yes|ok|sure|yeah)/i;
         this.NEGATIVE_REGEXP = /(no|negative)/i;
-        this.last_communication = new Date();
+        this.last_communication = moment();
     }
     /**
      * Check if the user is out of the office
@@ -27,13 +32,138 @@ var OOO_User = (function () {
      */
     OOO_User.prototype.isOOO = function () {
         var retVal = false;
-        var now = new Date();
-        retVal = (this.ooo_start && this.ooo_start < now);
+        var now = moment();
+        retVal = (this.ooo_start && this.ooo_start.isBefore(now));
         if (this.ooo_end) {
-            retVal = retVal && (this.ooo_end > now);
+            retVal = retVal && (this.ooo_end.isAfter(now));
         }
         return retVal;
     };
+    /**
+     * Gets the ms since last communication
+     *
+     * @return integer
+     */
+    OOO_User.prototype.lastCommunication = function () {
+        return this.last_communication ? moment().diff(this.last_communication) : 0;
+    };
+    /**
+     * Set the user's OOO message and return a response
+     *
+     * @param string message The message to set
+     * @return string A response for the user
+     */
+    OOO_User.prototype.setMessage = function (message) {
+        this.message = message;
+        return "Setting your OOO Message to:\n" + message;
+    };
+    /**
+     * Set the start of the user's OOO
+     *
+     * @param string start A parsable date/time string
+     * @return string A response for the user
+     */
+    OOO_User.prototype.setStart = function (start) {
+        var retVal = "Unable to parse " + start + " into a valid date/time";
+        var time;
+        if (start) {
+            time = this.parseDate(start);
+        }
+        else {
+            time = moment();
+        }
+        if (time.isValid()) {
+            this.ooo_start = time;
+            retVal = "You " + (time.isBefore() ? 'are' : 'will be') + " marked Out of Office at " + time.calendar();
+        }
+        return retVal;
+    };
+    /**
+     * Set the end of the user's OOO
+     *
+     * @param string end A parsable date/time string
+     * @return string A response for the user
+     */
+    OOO_User.prototype.setEnd = function (end) {
+        var retVal = "Unable to parse " + end + " into a valid date/time";
+        var time;
+        if (end) {
+            time = this.parseDate(end);
+        }
+        else {
+            time = moment();
+        }
+        if (time.isValid()) {
+            this.ooo_end = time;
+            if (time.isBefore()) {
+                retVal = 'You are no longer marked Out of Office';
+            }
+            else {
+                if (!this.ooo_start) {
+                    // Set the start time to now
+                    this.ooo_start = moment();
+                }
+                retVal = "You are marked Out of Office returning on " + time.calendar();
+            }
+        }
+        return retVal;
+    };
+    /**
+     * Parse a string into a moment date.
+     *
+     * @param string strDate The date string
+     * @return Moment
+     */
+    OOO_User.prototype.parseDate = function (strDate) {
+        var pDate = chrono.parseDate(strDate);
+        return pDate ? moment(pDate) : moment.invalid();
+    };
+    /**
+     * Parse any commands and their values from a message.
+     *
+     * @param string message The raw message
+     * @return string[]
+     */
+    OOO_User.prototype.parseCommands = function (message) {
+        var retVal = {};
+        var splits = message.split(/(start:|end:|message:)/);
+        var curCommand;
+        for (var x in splits) {
+            switch (splits[x].toLowerCase()) {
+                case 'message:':
+                case 'start:':
+                case 'end:':
+                    curCommand = splits[x].toLowerCase().replace(':', '');
+                    break;
+                default:
+                    if (curCommand) {
+                        retVal[curCommand] = splits[x].trim();
+                    }
+            }
+        }
+        return retVal;
+    };
+    /**
+     * Return some help flavor text.
+     *
+     * @return string
+     */
+    OOO_User.prototype.getHelp = function () {
+        var retVal = '';
+        retVal = '*Out of Office Bot*\n\n';
+        retVal += 'I can keep track of when you are out of the office and tell people that mention you.\n\n';
+        retVal += '*Usage*:\n';
+        retVal += 'To set yourself out of office, say hello and follow my prompts!\n';
+        retVal += 'To return to the office once you are back, say hello again!\n\n';
+        retVal += '*Direct Commands:*\n';
+        retVal += '- message: _string_, To set your Out of Office message\n';
+        retVal += '           Example: `message: I am out of the office`\n';
+        retVal += '- start:   _string_, A parsable date/time string when your Out of Office begins\n';
+        retVal += '           Example: `start: 2015-06-06 8:00`\n';
+        retVal += '- end:     _string_, A parsable date/time string when your Out of Office ends\n';
+        retVal += '           Example: `end: 2015-06-06 16:00`\n';
+        return retVal;
+    };
     /**
      * Handle a direct message to the bot
      *
@@ -42,12 +172,27 @@ var OOO_User = (function () {
      */
     OOO_User.prototype.handleMessage = function (message) {
         var retVal = '';
-        if (message.match(/help/i)) {
-            retVal = '*Out of Office Bot*\n\n';
-            retVal += 'I can keep track of when you are out of the office and tell people that mention you.\n\n';
-            retVal += '*Usage*:\n';
-            retVal += 'To set yourself out of office, say hello and follow my prompts!\n';
-            retVal += 'To return to the office once you are back, say hello again!';
+        var commands = this.parseCommands(message);
+        if (message.match(/^help/i)) {
+            retVal = this.getHelp();
+        }
+        else if (Object.keys(commands).length) {
+            for (var command in commands) {
+                switch (command) {
+                    case this.COMMAND_MESSAGE:
+                        retVal += "-" + this.setMessage(commands[command]) + "\n";
+                        break;
+                    case this.COMMAND_START:
+                        retVal += "-" + this.setStart(commands[command]) + "\n";
+                        break;
+                    case this.COMMAND_END:
+                        retVal += "-" + this.setEnd(commands[command]) + "\n";
+                        break;
+                    default:
+                        retVal += "-Error: Unknown comand: " + command + "\n";
+                        logger.error("Unknown command: " + command);
+                }
+            }
         }
         else {
             switch (this.status) {
@@ -60,8 +205,8 @@ var OOO_User = (function () {
                 case this.STATUS_AWAITING_CONFIRMATION:
                     if (message.match(this.POSITIVE_REGEXP)) {
                         this.status = this.STATUS_AWAITING_MESSAGE;
-                        this.ooo_start = new Date();
-                        retVal = "Sweet. You are now marked Out of Office with no message.\n";
+                        this.setStart();
+                        retVal = "Sweet. You are now marked Out of Office starting now with no message.\n";
                         retVal += "If you would like to set your Out of Office message, send it to me now";
                     }
                     else if (message.match(this.NEGATIVE_REGEXP)) {
@@ -70,10 +215,9 @@ var OOO_User = (function () {
                     }
                     break;
                 case this.STATUS_AWAITING_MESSAGE:
-                    if ((new Date().getTime() - this.last_communication.getTime()) < this.MESSAGE_TIMEOUT) {
-                        this.message = message;
+                    if (this.lastCommunication() < this.MESSAGE_TIMEOUT) {
                         this.status = this.STATUS_REGISTERED;
-                        retVal = "Setting your OOO Message to:\n" + message;
+                        retVal = this.setMessage(message);
                     }
                     else {
                         // set status to registered and handle it again
@@ -86,7 +230,7 @@ var OOO_User = (function () {
                     this.status = this.STATUS_AWAITING_DEREGISTER;
                     break;
                 case this.STATUS_AWAITING_DEREGISTER:
-                    if ((new Date().getTime() - this.last_communication.getTime()) < this.MESSAGE_TIMEOUT) {
+                    if (this.lastCommunication() < this.MESSAGE_TIMEOUT) {
                         if (message.match(this.POSITIVE_REGEXP)) {
                             this.status = this.STATUS_UNCONFIRMED;
                             this.ooo_start = null;
@@ -107,7 +251,7 @@ var OOO_User = (function () {
                     this.status = this.STATUS_UNCONFIRMED;
             }
         }
-        this.last_communication = new Date();
+        this.last_communication = moment();
         return retVal;
     };
     return OOO_User;
diff --git a/release/js/spec/ooo_user.spec.js b/release/js/spec/ooo_user.spec.js
index 1b3ea24..e911702 100644
--- a/release/js/spec/ooo_user.spec.js
+++ b/release/js/spec/ooo_user.spec.js
@@ -1,14 +1,87 @@
 /// <reference path="../typings/tsd.d.ts" />
 var OOO_User = require('../lib/ooo_user');
+var moment = require('moment');
 describe('OOO_User', function () {
     it('should instantiate and set username', function () {
         var user = new OOO_User('foo');
         expect(user.username).toEqual('foo');
         expect(user.status).toEqual(user.STATUS_UNCONFIRMED);
     });
+    describe('Date Handling', function () {
+        var user;
+        var now = moment();
+        beforeEach(function () {
+            user = new OOO_User('foo');
+        });
+        it('should parse a date', function () {
+            expect(user.parseDate('2015-03-03').isValid()).toBeTruthy();
+            expect(user.parseDate('Next Tuesday').isValid()).toBeTruthy();
+            expect(user.parseDate('Yesterday').isValid()).toBeTruthy();
+            expect(user.parseDate('3pm').isValid()).toBeTruthy();
+        });
+        it('should show ms since last communication', function () {
+            user.last_communication = moment().subtract(10, 'minutes');
+            expect(user.lastCommunication()).toBeGreaterThan(0);
+        });
+        describe('Out of Office Start', function () {
+            it('should set the time to now if no string passed', function () {
+                user.setStart();
+                expect(moment.isMoment(user.ooo_start)).toBeTruthy();
+            });
+            it('should set the time to the time specified', function () {
+                var strTime = '1982-05-20';
+                var momentTime = user.parseDate(strTime);
+                user.setStart(strTime);
+                expect(user.ooo_start.isSame(momentTime)).toBeTruthy();
+            });
+        });
+        describe('Out of Office End', function () {
+            it('should set the time to now if no string passed', function () {
+                user.setEnd();
+                expect(moment.isMoment(user.ooo_end)).toBeTruthy();
+            });
+            it('should set the time to the time specified', function () {
+                var strTime = '1982-05-20';
+                var momentTime = user.parseDate(strTime);
+                user.setEnd(strTime);
+                expect(user.ooo_end.isSame(momentTime)).toBeTruthy();
+            });
+        });
+    });
+    describe('Command Parsing', function () {
+        var user;
+        beforeEach(function () {
+            user = new OOO_User('foo');
+        });
+        it('should return help info', function () {
+            expect(user.handleMessage('help')).toEqual(user.getHelp());
+        });
+        it('should parse the start command', function () {
+            expect(user.parseCommands('start: foo')).toEqual({
+                start: 'foo'
+            });
+        });
+        it('should parse the end command', function () {
+            expect(user.parseCommands('end: foo')).toEqual({
+                end: 'foo'
+            });
+        });
+        it('should parse the end command', function () {
+            expect(user.parseCommands('message: foo')).toEqual({
+                message: 'foo'
+            });
+        });
+        it('should parse multiple commands', function () {
+            expect(user.parseCommands('message: foo bar fizz buzz start: s end: e')).toEqual({
+                message: 'foo bar fizz buzz',
+                start: 's',
+                end: 'e'
+            });
+        });
+    });
     describe('Out of Office Flagging', function () {
         var user;
-        var now = new Date();
+        var now = moment();
         beforeEach(function () {
             user = new OOO_User('foo');
         });
@@ -16,15 +89,15 @@ describe('OOO_User', function () {
             expect(user.isOOO()).toBeFalsy();
         });
         it('should be out of office if start date is less than now', function () {
-            user.ooo_start = new Date();
-            user.ooo_start.setMinutes(now.getMinutes() - 10);
+            user.setStart();
+            user.ooo_start.subtract(10, 'minutes');
             expect(user.isOOO()).toBeTruthy();
         });
         it('should not be out of office if end date is less than now', function () {
-            user.ooo_start = new Date();
-            user.ooo_start.setMinutes(now.getMinutes() - 10);
-            user.ooo_end = new Date();
-            user.ooo_end.setMinutes(now.getMinutes() - 5);
+            user.setStart();
+            user.ooo_start.subtract(10, 'minutes');
+            user.setEnd();
+            user.ooo_end.subtract(5, 'minutes');
             expect(user.isOOO()).toBeFalsy();
         });
     });
@@ -33,12 +106,39 @@ describe('OOO_User', function () {
         beforeEach(function () {
             user = new OOO_User('foo');
         });
+        it('should react to the message command', function () {
+            spyOn(user, 'setMessage').and.callThrough();
+            user.handleMessage('message: foo');
+            expect(user.setMessage).toHaveBeenCalled();
+            expect(user.message).toEqual('foo');
+        });
+        it('should react to the start command', function () {
+            spyOn(user, 'setStart').and.callThrough();
+            user.handleMessage('start: 2015-05-20');
+            expect(user.setStart).toHaveBeenCalled();
+            expect(moment.isMoment(user.ooo_start)).toBeTruthy();
+        });
+        it('should react to the end command', function () {
+            spyOn(user, 'setEnd').and.callThrough();
+            user.handleMessage('end: 2015-05-20');
+            expect(user.setEnd).toHaveBeenCalled();
+            expect(moment.isMoment(user.ooo_end)).toBeTruthy();
+        });
+        it('should react to multiple commands', function () {
+            spyOn(user, 'setMessage').and.callThrough();
+            spyOn(user, 'setStart').and.callThrough();
+            spyOn(user, 'setEnd').and.callThrough();
+            user.handleMessage('message: foo bar fizz buzz start: s end: e');
+            expect(user.setMessage).toHaveBeenCalled();
+            expect(user.setStart).toHaveBeenCalled();
+            expect(user.setEnd).toHaveBeenCalled();
+        });
         it('should set the last communication time after each message', function () {
-            user.last_communication.setMinutes(user.last_communication.getMinutes() - 10);
+            user.last_communication.subtract(10, 'minutes');
             var last_comm = user.last_communication;
             user.handleMessage('foo');
             expect(typeof user.last_communication).toEqual('object');
-            expect(user.last_communication.getTime()).toBeGreaterThan(last_comm.getTime());
+            expect(user.last_communication.isAfter(last_comm)).toBeTruthy();
         });
         it('should transition from unconfirmed to awaiting confirmation', function () {
             expect(user.status).toEqual(user.STATUS_UNCONFIRMED);
@@ -58,7 +158,7 @@ describe('OOO_User', function () {
         });
         it('should transition from awaiting message to registered after receiveing a message', function () {
             var message = 'This is my message';
-            user.last_communication = new Date();
+            user.last_communication = moment();
             user.status = user.STATUS_AWAITING_MESSAGE;
             user.handleMessage(message);
             expect(user.status).toEqual(user.STATUS_REGISTERED);
@@ -66,8 +166,7 @@ describe('OOO_User', function () {
         });
         it('should not accept a message after the timeout', function () {
             var message = 'This is my message';
-            var aWhileAgo = new Date();
-            aWhileAgo.setMinutes(aWhileAgo.getMinutes() - 10);
+            var aWhileAgo = moment().subtract(10, 'minutes');
             user.status = user.STATUS_AWAITING_MESSAGE;
             user.last_communication = aWhileAgo;
             user.handleMessage(message);
diff --git a/src/lib/ooo_user.ts b/src/lib/ooo_user.ts
index 54ecdcc..bdf490c 100644
--- a/src/lib/ooo_user.ts
+++ b/src/lib/ooo_user.ts
@@ -1,6 +1,8 @@
 /// <reference path="../typings/tsd.d.ts" />
 
 import logger = require('./logger');
+import moment = require('moment');
+var chrono = require('chrono-node');
 
 class OOO_User {
   STATUS_UNCONFIRMED            = 'unconfirmed';
@@ -9,17 +11,21 @@ class OOO_User {
   STATUS_REGISTERED             = 'registered';
   STATUS_AWAITING_DEREGISTER    = 'awaiting_deregister';
 
+  COMMAND_MESSAGE               = 'message';
+  COMMAND_START                 = 'start';
+  COMMAND_END                   = 'end';
+
   status: string = this.STATUS_UNCONFIRMED;
 
   message: string;
 
-  ooo_start: Date;
+  ooo_start: moment.Moment;
 
-  ooo_end: Date;
+  ooo_end: moment.Moment;
 
-  last_communication: Date;
+  last_communication: moment.Moment;
 
-  last_response: Date;
+  last_response: { [channel: string]: moment.Moment };
 
   MESSAGE_TIMEOUT = 60000; // Five minutes
   DEREGISTER_TIMEOUT = 60000; // Five minutes
@@ -33,7 +39,7 @@ class OOO_User {
    * @param string username the name of the user
    */
   constructor (public username: string) {
-    this.last_communication = new Date();
+    this.last_communication = moment();
   }
 
   /**
@@ -44,15 +50,156 @@ class OOO_User {
   isOOO (): boolean {
     var retVal = false;
 
-    var now = new Date();
-    retVal = (this.ooo_start && this.ooo_start < now);
+    var now = moment();
+    retVal = (this.ooo_start && this.ooo_start.isBefore(now));
     if (this.ooo_end) {
-      retVal = retVal && (this.ooo_end > now);
+      retVal = retVal && (this.ooo_end.isAfter(now));
+    }
+
+    return retVal;
+  }
+
+  /**
+   * Gets the ms since last communication
+   *
+   * @return integer
+   */
+  lastCommunication (): number {
+    return this.last_communication ? moment().diff(this.last_communication) : 0;
+  }
+
+  /**
+   * Set the user's OOO message and return a response
+   *
+   * @param string message The message to set
+   * @return string A response for the user
+   */
+  setMessage (message: string): string {
+    this.message = message;
+
+    return `Setting your OOO Message to:\n${message}`;
+  }
+
+  /**
+   * Set the start of the user's OOO
+   *
+   * @param string start A parsable date/time string
+   * @return string A response for the user
+   */
+  setStart (start?: string): string {
+    var retVal = `Unable to parse ${start} into a valid date/time`;
+    var time: moment.Moment;
+
+    if (start) {
+      time = this.parseDate(start);
+    } else {
+      time = moment();
+    }
+
+    if (time.isValid()) {
+      this.ooo_start = time;
+      retVal = `You ${time.isBefore() ? 'are' : 'will be'} marked Out of Office at ${time.calendar()}`;
     }
 
     return retVal;
   }
 
+  /**
+   * Set the end of the user's OOO
+   *
+   * @param string end A parsable date/time string
+   * @return string A response for the user
+   */
+  setEnd(end?: string): string {
+    var retVal = `Unable to parse ${end} into a valid date/time`;
+    var time: moment.Moment;
+
+    if (end) {
+      time = this.parseDate(end);
+    } else {
+      time = moment();
+    }
+
+    if (time.isValid()) {
+      this.ooo_end = time;
+      if (time.isBefore()) {
+        retVal = 'You are no longer marked Out of Office';
+      } else {
+        if (!this.ooo_start) {
+          // Set the start time to now
+          this.ooo_start = moment();
+        }
+        retVal = `You are marked Out of Office returning on ${time.calendar()}`;
+      }
+    }
+
+    return retVal;
+  }
+
+  /**
+   * Parse a string into a moment date.
+   *
+   * @param string strDate The date string
+   * @return Moment
+   */
+  parseDate (strDate: string): moment.Moment {
+    var pDate = chrono.parseDate(strDate);
+    return pDate ? moment(pDate) : moment.invalid();
+  }
+
+  /**
+   * Parse any commands and their values from a message.
+   *
+   * @param string message The raw message
+   * @return string[]
+   */
+  parseCommands (message: string): { [command: string]: string } {
+    var retVal: { [command: string]: string } = {};
+
+    var splits = message.split(/(start:|end:|message:)/);
+    var curCommand: string;
+
+    for (var x in splits) {
+      switch (splits[x].toLowerCase()) {
+        case 'message:':
+        case 'start:':
+        case 'end:':
+          curCommand = splits[x].toLowerCase().replace(':', '');
+          break;
+        default:
+          if (curCommand) {
+            retVal[curCommand] = splits[x].trim();
+          }
+      }
+    }
+
+    return retVal;
+  }
+
+  /**
+   * Return some help flavor text.
+   *
+   * @return string
+   */
+  getHelp (): string {
+    var retVal = '';
+
+    retVal = '*Out of Office Bot*\n\n';
+    retVal += 'I can keep track of when you are out of the office and tell people that mention you.\n\n';
+    retVal += '*Usage*:\n';
+    retVal += 'To set yourself out of office, say hello and follow my prompts!\n';
+    retVal += 'To return to the office once you are back, say hello again!\n\n';
+    retVal += '*Direct Commands:*\n';
+    retVal += '- message: _string_, To set your Out of Office message\n';
+    retVal += '           Example: `message: I am out of the office`\n';
+    retVal += '- start:   _string_, A parsable date/time string when your Out of Office begins\n';
+    retVal += '           Example: `start: 2015-06-06 8:00`\n';
+    retVal += '- end:     _string_, A parsable date/time string when your Out of Office ends\n';
+    retVal += '           Example: `end: 2015-06-06 16:00`\n';
+
+    return retVal;
+  }
+
   /**
    * Handle a direct message to the bot
    *
@@ -61,13 +208,27 @@ class OOO_User {
    */
   handleMessage (message: string): string {
     var retVal = '';
+    var commands = this.parseCommands(message);
 
-    if (message.match(/help/i)) {
-      retVal = '*Out of Office Bot*\n\n';
-      retVal += 'I can keep track of when you are out of the office and tell people that mention you.\n\n';
-      retVal += '*Usage*:\n';
-      retVal += 'To set yourself out of office, say hello and follow my prompts!\n';
-      retVal += 'To return to the office once you are back, say hello again!';
+    if (message.match(/^help/i)) {
+      retVal = this.getHelp();
+    } else if (Object.keys(commands).length) {
+      for (var command in commands) {
+        switch (command) {
+          case this.COMMAND_MESSAGE:
+            retVal += `-${this.setMessage(commands[command])}\n`;
+            break;
+          case this.COMMAND_START:
+            retVal += `-${this.setStart(commands[command])}\n`;
+            break;
+          case this.COMMAND_END:
+            retVal += `-${this.setEnd(commands[command])}\n`;
+            break;
+          default:
+            retVal += `-Error: Unknown comand: ${command}\n`;
+            logger.error(`Unknown command: ${command}`);
+        }
+      }
     } else {
       switch (this.status) {
         case this.STATUS_UNCONFIRMED:
@@ -78,24 +239,23 @@ class OOO_User {
           break;
         case this.STATUS_AWAITING_CONFIRMATION:
           if (message.match(this.POSITIVE_REGEXP)) {
-              this.status = this.STATUS_AWAITING_MESSAGE;
-              this.ooo_start = new Date();
-              retVal = "Sweet. You are now marked Out of Office with no message.\n";
-              retVal += "If you would like to set your Out of Office message, send it to me now";
+            this.status = this.STATUS_AWAITING_MESSAGE;
+            this.setStart();
+            retVal = "Sweet. You are now marked Out of Office starting now with no message.\n";
+            retVal += "If you would like to set your Out of Office message, send it to me now";
           } else if (message.match(this.NEGATIVE_REGEXP)) {
-              this.status = this.STATUS_UNCONFIRMED;
-              retVal = `Fine. Be that way`;
+            this.status = this.STATUS_UNCONFIRMED;
+            retVal = `Fine. Be that way`;
           }
           break;
         case this.STATUS_AWAITING_MESSAGE:
-          if ((new Date().getTime() - this.last_communication.getTime()) < this.MESSAGE_TIMEOUT) {
-              this.message = message;
-              this.status = this.STATUS_REGISTERED;
-              retVal = `Setting your OOO Message to:\n${message}`;
+          if (this.lastCommunication() < this.MESSAGE_TIMEOUT) {
+            this.status = this.STATUS_REGISTERED;
+            retVal = this.setMessage(message);
           } else {
-              // set status to registered and handle it again
-              this.status = this.STATUS_REGISTERED;
-              retVal = this.handleMessage(message);
+            // set status to registered and handle it again
+            this.status = this.STATUS_REGISTERED;
+            retVal = this.handleMessage(message);
           }
             break;
         case this.STATUS_REGISTERED:
@@ -103,18 +263,18 @@ class OOO_User {
           this.status = this.STATUS_AWAITING_DEREGISTER;
           break;
         case this.STATUS_AWAITING_DEREGISTER:
-          if ((new Date().getTime() - this.last_communication.getTime()) < this.MESSAGE_TIMEOUT) {
-              if (message.match(this.POSITIVE_REGEXP)) {
-                  this.status = this.STATUS_UNCONFIRMED;
-                  this.ooo_start = null;
-                  this.ooo_end = null;
-                  retVal = `Welcome back! You are no longer marked as out of the office.`;
-              } else if (message.match(this.NEGATIVE_REGEXP)) {
-                  this.status = this.STATUS_REGISTERED;
-                  retVal = `Ok, then get out of here!`;
-              }
+          if (this.lastCommunication() < this.MESSAGE_TIMEOUT) {
+            if (message.match(this.POSITIVE_REGEXP)) {
+              this.status = this.STATUS_UNCONFIRMED;
+              this.ooo_start = null;
+              this.ooo_end = null;
+              retVal = `Welcome back! You are no longer marked as out of the office.`;
+            } else if (message.match(this.NEGATIVE_REGEXP)) {
+              this.status = this.STATUS_REGISTERED;
+              retVal = `Ok, then get out of here!`;
+            }
           } else {
-              retVal = "I haven't heard from you in a while? Are you trying to return to the office? [Yes/No]";
+            retVal = "I haven't heard from you in a while? Are you trying to return to the office? [Yes/No]";
           }
 
           break;
@@ -124,7 +284,7 @@ class OOO_User {
       }
     }
 
-    this.last_communication = new Date();
+    this.last_communication = moment();
 
     return retVal;
   }
diff --git a/src/spec/ooo_user.spec.ts b/src/spec/ooo_user.spec.ts
index 4b68975..5eb095f 100644
--- a/src/spec/ooo_user.spec.ts
+++ b/src/spec/ooo_user.spec.ts
@@ -1,6 +1,7 @@
 /// <reference path="../typings/tsd.d.ts" />
 
 import OOO_User = require('../lib/ooo_user');
+import moment = require('moment');
 
 describe ('OOO_User', () => {
   it('should instantiate and set username', () => {
@@ -9,9 +10,97 @@ describe ('OOO_User', () => {
     expect(user.status).toEqual(user.STATUS_UNCONFIRMED);
   })
 
+  describe('Date Handling', () => {
+    var user: OOO_User;
+    var now = moment();
+
+    beforeEach(() => {
+      user = new OOO_User('foo');
+    })
+
+    it('should parse a date', () => {
+      expect(user.parseDate('2015-03-03').isValid()).toBeTruthy();
+      expect(user.parseDate('Next Tuesday').isValid()).toBeTruthy();
+      expect(user.parseDate('Yesterday').isValid()).toBeTruthy();
+      expect(user.parseDate('3pm').isValid()).toBeTruthy();
+      expect(user.parseDate('derp').isValid()).toBeFalsy();
+    })
+
+    it('should show ms since last communication', () => {
+      user.last_communication = moment().subtract(10, 'minutes');
+      expect(user.lastCommunication()).toBeGreaterThan(0);
+    })
+
+    describe('Out of Office Start', () => {
+      it('should set the time to now if no string passed', () => {
+        user.setStart();
+        expect(moment.isMoment(user.ooo_start)).toBeTruthy();
+      })
+
+      it('should set the time to the time specified', () => {
+        var strTime = '1982-05-20';
+        var momentTime = user.parseDate(strTime);
+        user.setStart(strTime);
+        expect(user.ooo_start.isSame(momentTime)).toBeTruthy();
+      })
+    })
+
+    describe('Out of Office End', () => {
+        it('should set the time to now if no string passed', () => {
+            user.setEnd();
+            expect(moment.isMoment(user.ooo_end)).toBeTruthy();
+        })
+
+        it('should set the time to the time specified', () => {
+            var strTime = '1982-05-20';
+            var momentTime = user.parseDate(strTime);
+            user.setEnd(strTime);
+            expect(user.ooo_end.isSame(momentTime)).toBeTruthy();
+        })
+    })
+  });
+
+  describe('Command Parsing', () => {
+    var user: OOO_User;
+
+    beforeEach(() => {
+      user = new OOO_User('foo');
+    })
+
+    it('should return help info', () => {
+      expect(user.handleMessage('help')).toEqual(user.getHelp());
+    })
+
+    it('should parse the start command', () => {
+      expect(user.parseCommands('start: foo')).toEqual({
+        start: 'foo'
+      })
+    })
+
+    it('should parse the end command', () => {
+      expect(user.parseCommands('end: foo')).toEqual({
+        end: 'foo'
+      })
+    })
+
+    it('should parse the end command', () => {
+      expect(user.parseCommands('message: foo')).toEqual({
+        message: 'foo'
+      })
+    })
+
+    it('should parse multiple commands', () => {
+      expect(user.parseCommands('message: foo bar fizz buzz start: s end: e')).toEqual({
+        message: 'foo bar fizz buzz',
+        start: 's',
+        end: 'e'
+      })
+    })
+  })
+
   describe('Out of Office Flagging', () => {
     var user: OOO_User;
-    var now = new Date();
+    var now = moment();
 
     beforeEach(() => {
       user = new OOO_User('foo');
@@ -22,18 +111,18 @@ describe ('OOO_User', () => {
     })
 
     it('should be out of office if start date is less than now', () => {
-      user.ooo_start = new Date();
-      user.ooo_start.setMinutes(now.getMinutes() - 10);
+      user.setStart();
+      user.ooo_start.subtract(10, 'minutes');
 
       expect(user.isOOO()).toBeTruthy();
     })
 
     it('should not be out of office if end date is less than now', () => {
-      user.ooo_start = new Date();
-      user.ooo_start.setMinutes(now.getMinutes() - 10);
+      user.setStart();
+      user.ooo_start.subtract(10, 'minutes');
 
-      user.ooo_end = new Date();
-      user.ooo_end.setMinutes(now.getMinutes() - 5);
+      user.setEnd();
+      user.ooo_end.subtract(5, 'minutes');
 
       expect(user.isOOO()).toBeFalsy();
     })
@@ -46,13 +135,45 @@ describe ('OOO_User', () => {
       user = new OOO_User('foo');
     })
 
+    it('should react to the message command', () => {
+      spyOn(user, 'setMessage').and.callThrough();
+      user.handleMessage('message: foo');
+      expect(user.setMessage).toHaveBeenCalled();
+      expect(user.message).toEqual('foo');
+    })
+
+    it('should react to the start command', () => {
+      spyOn(user, 'setStart').and.callThrough();
+      user.handleMessage('start: 2015-05-20');
+      expect(user.setStart).toHaveBeenCalled();
+      expect(moment.isMoment(user.ooo_start)).toBeTruthy();
+    })
+
+    it('should react to the end command', () => {
+      spyOn(user, 'setEnd').and.callThrough();
+      user.handleMessage('end: 2015-05-20');
+      expect(user.setEnd).toHaveBeenCalled();
+      expect(moment.isMoment(user.ooo_end)).toBeTruthy();
+    })
+
+    it('should react to multiple commands', () => {
+      spyOn(user, 'setMessage').and.callThrough();
+      spyOn(user, 'setStart').and.callThrough();
+      spyOn(user, 'setEnd').and.callThrough();
+
+      user.handleMessage('message: foo bar fizz buzz start: s end: e');
+      expect(user.setMessage).toHaveBeenCalled();
+      expect(user.setStart).toHaveBeenCalled();
+      expect(user.setEnd).toHaveBeenCalled();
+    })
+
     it('should set the last communication time after each message', () => {
-      user.last_communication.setMinutes(user.last_communication.getMinutes() - 10);
+      user.last_communication.subtract(10, 'minutes');
       var last_comm = user.last_communication
 
       user.handleMessage('foo');
       expect(typeof user.last_communication).toEqual('object');
-      expect(user.last_communication.getTime()).toBeGreaterThan(last_comm.getTime());
+      expect(user.last_communication.isAfter(last_comm)).toBeTruthy();
     })
 
     it('should transition from unconfirmed to awaiting confirmation', () => {
@@ -77,7 +198,7 @@ describe ('OOO_User', () => {
 
     it('should transition from awaiting message to registered after receiveing a message', () => {
       var message = 'This is my message';
-      user.last_communication = new Date();
+      user.last_communication = moment();
 
       user.status = user.STATUS_AWAITING_MESSAGE;
       user.handleMessage(message);
@@ -87,8 +208,7 @@ describe ('OOO_User', () => {
 
     it('should not accept a message after the timeout', () => {
       var message = 'This is my message';
-      var aWhileAgo = new Date();
-      aWhileAgo.setMinutes(aWhileAgo.getMinutes() - 10);
+      var aWhileAgo = moment().subtract(10, 'minutes');
 
       user.status = user.STATUS_AWAITING_MESSAGE;
       user.last_communication = aWhileAgo;
-- 
GitLab