lib/botkit/CoreBot.js
function Conversation(task, message) {

    this.messages = [];
    this.sent = [];
    this.transcript = [];

    this.context = {
      user: message.user,
      channel: message.channel,
      bot: task.bot,
    };

    this.events = {};

    this.vars = {};

    this.threads = {};
    this.thread = null;

    this.status = 'new';
    this.task = task;
    this.source_message = message;
    this.handler = null;
    this.responses = {};
    this.capture_options = {};
    this.startTime = new Date();
    this.lastActive = new Date();
    /** will be pointing to a callback which will be called after timeout,
     * conversation will be not be ended and should be taken care by callback
     */
    this.timeOutHandler = null;

    this.collectResponse = function (key, value) {
      this.responses[key] = value;
    };

    this.capture = function (response, cb) {

      var that = this;
      var capture_key = this.sent[this.sent.length - 1].text;
      botkit.middleware.capture.run(that.task.bot, response, that, function (err, bot, response, convo) {
        if (response.text) {
          response.text = response.text.trim();
        } else {
          response.text = '';
        }

        if (that.capture_options.key != undefined) {
          capture_key = that.capture_options.key;
        }

        // capture the question that was asked
        // if text is an array, get 1st
        if (typeof (that.sent[that.sent.length - 1].text) == 'string') {
          response.question = that.sent[that.sent.length - 1].text;
        } else if (Array.isArray(that.sent[that.sent.length - 1].text)) {
          response.question = that.sent[that.sent.length - 1].text[0];
        } else {
          response.question = '';
        }

        if (that.capture_options.multiple) {
          if (!that.responses[capture_key]) {
            that.responses[capture_key] = [];
          }
          that.responses[capture_key].push(response);
        } else {
          that.responses[capture_key] = response;
        }

        if (cb) cb(response);
      });

    };

    this.handle = function (message) {

      var that = this;
      this.lastActive = new Date();
      this.transcript.push(message);
      botkit.logger.debug('HANDLING MESSAGE IN CONVO', message);
      // do other stuff like call custom callbacks
      if (this.handler) {
        this.capture(message, function (message) {
          // if the handler is a normal function, just execute it!
          // NOTE: anyone who passes in their own handler has to call
          // convo.next() to continue after completing whatever it is they want to do.
          if (typeof (that.handler) == 'function') {
            that.handler(message, that);
          } else {
            // handle might be a mapping of keyword to callback.
            // lets see if the message matches any of the keywords
            let match, patterns = that.handler;
            for (let p = 0; p < patterns.length; p++) {
              if (patterns[p].pattern && botkit.hears_test([patterns[p].pattern], message)) {
                botkit.middleware.heard.run(that.task.bot, message, function (err, bot, message) {
                  patterns[p].callback(message, that);
                });
                return;
              }
            }

            // none of the messages matched! What do we do?
            // if a default exists, fire it!
            for (let p = 0; p < patterns.length; p++) {
              if (patterns[p].default) {
                botkit.middleware.heard.run(that.task.bot, message, function (err, bot, message) {
                  patterns[p].callback(message, that);
                });
                return;
              }
            }

          }
        });
      } else {
        // do nothing
      }
    };

    this.setVar = function (field, value) {
      if (!this.vars) {
        this.vars = {};
      }
      this.vars[field] = value;
    };

    this.activate = function () {
      this.task.trigger('conversationStarted', [this]);
      this.task.botkit.trigger('conversationStarted', [this.task.bot, this]);
      this.status = 'active';
    };

    /**
     * active includes both ACTIVE and ENDING
     * in order to allow the timeout end scripts to play out
     **/
    this.isActive = function () {
      return (this.status == 'active' || this.status == 'ending');
    };

    this.deactivate = function () {
      this.status = 'inactive';
    };

    this.say = function (message) {
      this.addMessage(message);
    };

    this.sayFirst = function (message) {
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel,
        };
      } else {
        message.channel = this.source_message.channel;
      }
      this.messages.unshift(message);
    };


    this.on = function (event, cb) {
      botkit.logger.debug('Setting up a handler for', event);
      var events = event.split(/\,/g);
      for (var e in events) {
        if (!this.events[events[e]]) {
          this.events[events[e]] = [];
        }
        this.events[events[e]].push(cb);
      }
      return this;
    };

    this.trigger = function (event, data) {
      if (this.events[event]) {
        for (var e = 0; e < this.events[event].length; e++) {
          var res = this.events[event][e].apply(this, data);
          if (res === false) {
            return;
          }
        }
      } else { }
    };

    // proceed to the next message after waiting for an answer
    this.next = function () {
      this.handler = null;
    };

    this.repeat = function () {
      if (this.sent.length) {
        this.messages.push(this.sent[this.sent.length - 1]);
      } else {
        // console.log('TRIED TO REPEAT, NOTHING TO SAY');
      }
    };

    this.silentRepeat = function () {
      return;
    };

    this.addQuestion = function (message, cb, capture_options, thread) {
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel
        };
      } else {
        message.channel = this.source_message.channel;
      }

      if (capture_options) {
        message.capture_options = capture_options;
      }

      message.handler = cb;
      this.addMessage(message, thread);
    };


    this.ask = function (message, cb, capture_options) {
      this.addQuestion(message, cb, capture_options, this.thread || 'default');
    };

    this.addMessage = function (message, thread) {
      if (!thread) {
        thread = this.thread;
      }
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel,
        };
      } else {
        message.channel = this.source_message.channel;
      }

      if (!this.threads[thread]) {
        this.threads[thread] = [];
      }
      this.threads[thread].push(message);

      // this is the current topic, so add it here as well
      if (this.thread == thread) {
        this.messages.push(message);
      }
    };

    // how long should the bot wait while a user answers?
    this.setTimeout = function (timeout) {
      this.task.timeLimit = timeout;
    };

    // For backwards compatibility, wrap gotoThread in its previous name
    this.changeTopic = function (topic) {
      this.gotoThread(topic);
    };

    this.hasThread = function (thread) {
      return (this.threads[thread] != undefined);
    };


    this.transitionTo = function (thread, message) {

      // add a new transition thread
      // add this new message to it
      // set that message action to execute the actual transition
      // then change threads to transition thread

      var num = 1;
      while (this.hasThread('transition_' + num)) {
        num++;
      }

      var threadname = 'transition_' + num;

      if (typeof (message) == 'string') {
        message = {
          text: message,
          action: thread
        };
      } else {
        message.action = thread;
      }

      this.addMessage(message, threadname);

      this.gotoThread(threadname);

    };

    this.beforeThread = function (thread, callback) {
      if (!this.before_hooks) {
        this.before_hooks = {};
      }

      if (!this.before_hooks[thread]) {
        this.before_hooks[thread] = [];
      }
      this.before_hooks[thread].push(callback);
    };

    this.gotoThread = function (thread) {
      var that = this;
      that.next_thread = thread;
      that.processing = true;

      var makeChange = function () {
        if (!that.hasThread(that.next_thread)) {
          if (that.next_thread == 'default') {
            that.threads[that.next_thread] = [];
          } else {
            botkit.logger.debug('WARN: gotoThread() to an invalid thread!', thread);
            that.stop('unknown_thread');
            return;
          }
        }

        that.thread = that.next_thread;
        that.messages = that.threads[that.next_thread].slice();

        that.handler = null;
        that.processing = false;
      };

      if (that.before_hooks && that.before_hooks[that.next_thread]) {

        // call any beforeThread hooks in sequence
        async.eachSeries(this.before_hooks[that.next_thread], function (before_hook, next) {
          before_hook(that, next);
        }, function (err) {
          if (!err) {
            makeChange();
          }
        });

      } else {

        makeChange();

      }

    };

    this.combineMessages = function (messages) {
      if (!messages) {
        return '';
      }

      if (Array.isArray(messages) && !messages.length) {
        return '';
      }

      if (messages.length > 1) {
        var txt = [];
        var last_user = null;
        var multi_users = false;
        last_user = messages[0].user;
        for (let x = 0; x < messages.length; x++) {
          if (messages[x].user != last_user) {
            multi_users = true;
          }
        }
        last_user = '';
        for (let x = 0; x < messages.length; x++) {
          if (multi_users && messages[x].user != last_user) {
            last_user = messages[x].user;
            if (txt.length) {
              txt.push('');
            }
            txt.push('<@' + messages[x].user + '>:');
          }
          txt.push(messages[x].text);
        }
        return txt.join('\n');
      } else {
        if (messages.length) {
          return messages[0].text;
        } else {
          return messages.text;
        }
      }
    };

    this.getResponses = function () {

      var res = {};
      for (var key in this.responses) {

        res[key] = {
          question: this.responses[key].length ?
            this.responses[key][0].question : this.responses[key].question,
          key: key,
          answer: this.extractResponse(key),
        };
      }
      return res;
    };

    this.getResponsesAsArray = function () {

      var res = [];
      for (var key in this.responses) {

        res.push({
          question: this.responses[key].length ?
            this.responses[key][0].question : this.responses[key].question,
          key: key,
          answer: this.extractResponse(key),
        });
      }
      return res;
    };


    this.extractResponses = function () {

      var res = {};
      for (var key in this.responses) {
        res[key] = this.extractResponse(key);
      }
      return res;
    };

    this.extractResponse = function (key) {
      return this.combineMessages(this.responses[key]);
    };

    this.replaceAttachmentTokens = function (attachments) {

      if (attachments && attachments.length) {
        for (let a = 0; a < attachments.length; a++) {
          for (let key in attachments[a]) {
            if (typeof (attachments[a][key]) == 'string') {
              attachments[a][key] = this.replaceTokens(attachments[a][key]);
            } else {
              attachments[a][key] = this.replaceAttachmentTokens(attachments[a][key]);
            }
          }
        }
      } else {
        for (let a in attachments) {
          if (typeof (attachments[a]) == 'string') {
            attachments[a] = this.replaceTokens(attachments[a]);
          } else {
            attachments[a] = this.replaceAttachmentTokens(attachments[a]);
          }
        }
      }

      return attachments;
    };


    this.replaceTokens = function (text) {

      var vars = {
        identity: this.task.bot.identity,
        responses: this.extractResponses(),
        origin: this.task.source_message,
        vars: this.vars,
      };

      var rendered = '';

      try {
        rendered = mustache.render(text, vars);
      } catch (err) {
        botkit.logger.error('Error in message template. Mustache failed with error: ', err);
        rendered = text;
      }

      return rendered;
    };

    this.stop = function (status) {
      this.handler = null;
      this.messages = [];
      this.status = status || 'stopped';
      botkit.logger.debug('Conversation is over with status ' + this.status);
      this.task.conversationEnded(this);
    };

    // was this conversation successful?
    // return true if it was completed
    // otherwise, return false
    // false could indicate a variety of failed states:
    // manually stopped, timed out, etc
    this.successful = function () {

      // if the conversation is still going, it can't be successful yet
      if (this.isActive()) {
        return false;
      }

      if (this.status == 'completed') {
        return true;
      } else {
        return false;
      }

    };

    this.cloneMessage = function (message) {
      // clone this object so as not to modify source
      var outbound = clone(message);

      if (typeof (message.text) == 'string') {
        outbound.text = this.replaceTokens(message.text);
      } else if (message.text) {
        outbound.text = this.replaceTokens(
          message.text[Math.floor(Math.random() * message.text.length)]
        );
      }

      if (outbound.attachments) {
        outbound.attachments = this.replaceAttachmentTokens(outbound.attachments);
      }

      if (outbound.attachment) {

        // pick one variation of the message text at random
        if (outbound.attachment.payload.text && typeof (outbound.attachment.payload.text) != 'string') {
          outbound.attachment.payload.text = this.replaceTokens(
            outbound.attachment.payload.text[
            Math.floor(Math.random() * outbound.attachment.payload.text.length)
            ]
          );
        }
        outbound.attachment = this.replaceAttachmentTokens([outbound.attachment])[0];
      }

      if (this.messages.length && !message.handler) {
        outbound.continue_typing = true;
      }

      if (typeof (message.attachments) == 'function') {
        outbound.attachments = message.attachments(this);
      }

      return outbound;
    };

    this.onTimeout = function (handler) {
      if (typeof (handler) == 'function') {
        this.timeOutHandler = handler;
      } else {
        botkit.logger.debug('Invalid timeout function passed to onTimeout');
      }
    };

    this.tick = function () {
      var now = new Date();

      if (this.isActive()) {
        if (this.processing) {
          // do nothing. The bot is waiting for async process to complete.
        } else if (this.handler) {
          // check timeout!
          // how long since task started?
          var duration = (now.getTime() - this.task.startTime.getTime());
          // how long since last active?
          var lastActive = (now.getTime() - this.lastActive.getTime());

          if (this.task.timeLimit && // has a timelimit
            (duration > this.task.timeLimit) && // timelimit is up
            (lastActive > this.task.timeLimit) // nobody has typed for 60 seconds at least
          ) {
            // if timeoutHandler is set then call it, otherwise follow the normal flow
            // this will not break others code, after the update
            if (this.timeOutHandler) {
              this.timeOutHandler(this);
            } else if (this.hasThread('on_timeout')) {
              this.status = 'ending';
              this.gotoThread('on_timeout');
            } else {
              this.stop('timeout');
            }
          }
          // otherwise do nothing
        } else {
          if (this.messages.length) {

            if (this.sent.length &&
              !this.sent[this.sent.length - 1].sent
            ) {
              return;
            }

            if (this.task.bot.botkit.config.require_delivery &&
              this.sent.length &&
              !this.sent[this.sent.length - 1].delivered
            ) {
              return;
            }

            if (typeof (this.messages[0].timestamp) == 'undefined' ||
              this.messages[0].timestamp <= now.getTime()) {
              var message = this.messages.shift();
              //console.log('HANDLING NEW MESSAGE',message);
              // make sure next message is delayed appropriately
              if (this.messages.length && this.messages[0].delay) {
                this.messages[0].timestamp = now.getTime() + this.messages[0].delay;
              }
              if (message.handler) {
                //console.log(">>>>>> SET HANDLER IN TICK");
                this.handler = message.handler;
              } else {
                this.handler = null;
                //console.log(">>>>>>> CLEARING HANDLER BECAUSE NO HANDLER NEEDED");
              }
              if (message.capture_options) {
                this.capture_options = message.capture_options;
              } else {
                this.capture_options = {};
              }

              this.lastActive = new Date();

              // is there any text?
              // or an attachment? (facebook)
              // or multiple attachments (slack)
              // if (message.text || message.attachments || message.attachment) {
              if (message) {

                var outbound = this.cloneMessage(message);
                var that = this;

                outbound.sent_timestamp = new Date().getTime();

                that.sent.push(outbound);
                that.transcript.push(outbound);

                this.task.bot.reply(this.source_message, outbound, function (err, sent_message) {
                  if (err) {
                    botkit.logger.error('An error occurred while sending a message: ', err);

                    // even though an error occured, set sent to true
                    // this will allow the conversation to keep going even if one message fails
                    // TODO: make a message that fails to send _resend_ at least once
                    that.sent[that.sent.length - 1].sent = true;
                    that.sent[that.sent.length - 1].api_response = err;

                  } else {

                    that.sent[that.sent.length - 1].sent = true;
                    that.sent[that.sent.length - 1].api_response = sent_message;

                    // if sending via slack's web api, there is no further confirmation
                    // so we can mark the message delivered
                    if (that.task.bot.type == 'slack' && sent_message && sent_message.ts) {
                      that.sent[that.sent.length - 1].delivered = true;
                    }

                    that.trigger('sent', [sent_message]);

                  }
                });
              }
              if (message.action) {
                if (typeof (message.action) == 'function') {
                  message.action(this);
                } else if (message.action == 'repeat') {
                  this.repeat();
                } else if (message.action == 'wait') {
                  this.silentRepeat();
                } else if (message.action == 'stop') {
                  this.stop();
                } else if (message.action == 'timeout') {
                  this.stop('timeout');
                } else if (this.threads[message.action]) {
                  this.gotoThread(message.action);
                }
              }
            } else {
              //console.log('Waiting to send next message...');
            }

            // end immediately instad of waiting til next tick.
            // if it hasn't already been ended by a message action!
            if (this.isActive() && !this.messages.length && !this.handler && !this.processing) {
              this.stop('completed');
            }

          } else if (this.sent.length) { // sent at least 1 message
            this.stop('completed');
          }
        }
      }
    };

    botkit.logger.debug('CREATED A CONVO FOR', this.source_message.user, this.source_message.channel);
    this.gotoThread('default');
  }
function Conversation(task, message) {

    this.messages = [];
    this.sent = [];
    this.transcript = [];

    this.context = {
      user: message.user,
      channel: message.channel,
      bot: task.bot,
    };

    this.events = {};

    this.vars = {};

    this.threads = {};
    this.thread = null;

    this.status = 'new';
    this.task = task;
    this.source_message = message;
    this.handler = null;
    this.responses = {};
    this.capture_options = {};
    this.startTime = new Date();
    this.lastActive = new Date();
    /** will be pointing to a callback which will be called after timeout,
     * conversation will be not be ended and should be taken care by callback
     */
    this.timeOutHandler = null;

    this.collectResponse = function (key, value) {
      this.responses[key] = value;
    };

    this.capture = function (response, cb) {

      var that = this;
      var capture_key = this.sent[this.sent.length - 1].text;
      botkit.middleware.capture.run(that.task.bot, response, that, function (err, bot, response, convo) {
        if (response.text) {
          response.text = response.text.trim();
        } else {
          response.text = '';
        }

        if (that.capture_options.key != undefined) {
          capture_key = that.capture_options.key;
        }

        // capture the question that was asked
        // if text is an array, get 1st
        if (typeof (that.sent[that.sent.length - 1].text) == 'string') {
          response.question = that.sent[that.sent.length - 1].text;
        } else if (Array.isArray(that.sent[that.sent.length - 1].text)) {
          response.question = that.sent[that.sent.length - 1].text[0];
        } else {
          response.question = '';
        }

        if (that.capture_options.multiple) {
          if (!that.responses[capture_key]) {
            that.responses[capture_key] = [];
          }
          that.responses[capture_key].push(response);
        } else {
          that.responses[capture_key] = response;
        }

        if (cb) cb(response);
      });

    };

    this.handle = function (message) {

      var that = this;
      this.lastActive = new Date();
      this.transcript.push(message);
      botkit.logger.debug('HANDLING MESSAGE IN CONVO', message);
      // do other stuff like call custom callbacks
      if (this.handler) {
        this.capture(message, function (message) {
          // if the handler is a normal function, just execute it!
          // NOTE: anyone who passes in their own handler has to call
          // convo.next() to continue after completing whatever it is they want to do.
          if (typeof (that.handler) == 'function') {
            that.handler(message, that);
          } else {
            // handle might be a mapping of keyword to callback.
            // lets see if the message matches any of the keywords
            let match, patterns = that.handler;
            for (let p = 0; p < patterns.length; p++) {
              if (patterns[p].pattern && botkit.hears_test([patterns[p].pattern], message)) {
                botkit.middleware.heard.run(that.task.bot, message, function (err, bot, message) {
                  patterns[p].callback(message, that);
                });
                return;
              }
            }

            // none of the messages matched! What do we do?
            // if a default exists, fire it!
            for (let p = 0; p < patterns.length; p++) {
              if (patterns[p].default) {
                botkit.middleware.heard.run(that.task.bot, message, function (err, bot, message) {
                  patterns[p].callback(message, that);
                });
                return;
              }
            }

          }
        });
      } else {
        // do nothing
      }
    };

    this.setVar = function (field, value) {
      if (!this.vars) {
        this.vars = {};
      }
      this.vars[field] = value;
    };

    this.activate = function () {
      this.task.trigger('conversationStarted', [this]);
      this.task.botkit.trigger('conversationStarted', [this.task.bot, this]);
      this.status = 'active';
    };

    /**
     * active includes both ACTIVE and ENDING
     * in order to allow the timeout end scripts to play out
     **/
    this.isActive = function () {
      return (this.status == 'active' || this.status == 'ending');
    };

    this.deactivate = function () {
      this.status = 'inactive';
    };

    this.say = function (message) {
      this.addMessage(message);
    };

    this.sayFirst = function (message) {
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel,
        };
      } else {
        message.channel = this.source_message.channel;
      }
      this.messages.unshift(message);
    };


    this.on = function (event, cb) {
      botkit.logger.debug('Setting up a handler for', event);
      var events = event.split(/\,/g);
      for (var e in events) {
        if (!this.events[events[e]]) {
          this.events[events[e]] = [];
        }
        this.events[events[e]].push(cb);
      }
      return this;
    };

    this.trigger = function (event, data) {
      if (this.events[event]) {
        for (var e = 0; e < this.events[event].length; e++) {
          var res = this.events[event][e].apply(this, data);
          if (res === false) {
            return;
          }
        }
      } else { }
    };

    // proceed to the next message after waiting for an answer
    this.next = function () {
      this.handler = null;
    };

    this.repeat = function () {
      if (this.sent.length) {
        this.messages.push(this.sent[this.sent.length - 1]);
      } else {
        // console.log('TRIED TO REPEAT, NOTHING TO SAY');
      }
    };

    this.silentRepeat = function () {
      return;
    };

    this.addQuestion = function (message, cb, capture_options, thread) {
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel
        };
      } else {
        message.channel = this.source_message.channel;
      }

      if (capture_options) {
        message.capture_options = capture_options;
      }

      message.handler = cb;
      this.addMessage(message, thread);
    };


    this.ask = function (message, cb, capture_options) {
      this.addQuestion(message, cb, capture_options, this.thread || 'default');
    };

    this.addMessage = function (message, thread) {
      if (!thread) {
        thread = this.thread;
      }
      if (typeof (message) == 'string') {
        message = {
          text: message,
          channel: this.source_message.channel,
        };
      } else {
        message.channel = this.source_message.channel;
      }

      if (!this.threads[thread]) {
        this.threads[thread] = [];
      }
      this.threads[thread].push(message);

      // this is the current topic, so add it here as well
      if (this.thread == thread) {
        this.messages.push(message);
      }
    };

    // how long should the bot wait while a user answers?
    this.setTimeout = function (timeout) {
      this.task.timeLimit = timeout;
    };

    // For backwards compatibility, wrap gotoThread in its previous name
    this.changeTopic = function (topic) {
      this.gotoThread(topic);
    };

    this.hasThread = function (thread) {
      return (this.threads[thread] != undefined);
    };


    this.transitionTo = function (thread, message) {

      // add a new transition thread
      // add this new message to it
      // set that message action to execute the actual transition
      // then change threads to transition thread

      var num = 1;
      while (this.hasThread('transition_' + num)) {
        num++;
      }

      var threadname = 'transition_' + num;

      if (typeof (message) == 'string') {
        message = {
          text: message,
          action: thread
        };
      } else {
        message.action = thread;
      }

      this.addMessage(message, threadname);

      this.gotoThread(threadname);

    };

    this.beforeThread = function (thread, callback) {
      if (!this.before_hooks) {
        this.before_hooks = {};
      }

      if (!this.before_hooks[thread]) {
        this.before_hooks[thread] = [];
      }
      this.before_hooks[thread].push(callback);
    };

    this.gotoThread = function (thread) {
      var that = this;
      that.next_thread = thread;
      that.processing = true;

      var makeChange = function () {
        if (!that.hasThread(that.next_thread)) {
          if (that.next_thread == 'default') {
            that.threads[that.next_thread] = [];
          } else {
            botkit.logger.debug('WARN: gotoThread() to an invalid thread!', thread);
            that.stop('unknown_thread');
            return;
          }
        }

        that.thread = that.next_thread;
        that.messages = that.threads[that.next_thread].slice();

        that.handler = null;
        that.processing = false;
      };

      if (that.before_hooks && that.before_hooks[that.next_thread]) {

        // call any beforeThread hooks in sequence
        async.eachSeries(this.before_hooks[that.next_thread], function (before_hook, next) {
          before_hook(that, next);
        }, function (err) {
          if (!err) {
            makeChange();
          }
        });

      } else {

        makeChange();

      }

    };

    this.combineMessages = function (messages) {
      if (!messages) {
        return '';
      }

      if (Array.isArray(messages) && !messages.length) {
        return '';
      }

      if (messages.length > 1) {
        var txt = [];
        var last_user = null;
        var multi_users = false;
        last_user = messages[0].user;
        for (let x = 0; x < messages.length; x++) {
          if (messages[x].user != last_user) {
            multi_users = true;
          }
        }
        last_user = '';
        for (let x = 0; x < messages.length; x++) {
          if (multi_users && messages[x].user != last_user) {
            last_user = messages[x].user;
            if (txt.length) {
              txt.push('');
            }
            txt.push('<@' + messages[x].user + '>:');
          }
          txt.push(messages[x].text);
        }
        return txt.join('\n');
      } else {
        if (messages.length) {
          return messages[0].text;
        } else {
          return messages.text;
        }
      }
    };

    this.getResponses = function () {

      var res = {};
      for (var key in this.responses) {

        res[key] = {
          question: this.responses[key].length ?
            this.responses[key][0].question : this.responses[key].question,
          key: key,
          answer: this.extractResponse(key),
        };
      }
      return res;
    };

    this.getResponsesAsArray = function () {

      var res = [];
      for (var key in this.responses) {

        res.push({
          question: this.responses[key].length ?
            this.responses[key][0].question : this.responses[key].question,
          key: key,
          answer: this.extractResponse(key),
        });
      }
      return res;
    };


    this.extractResponses = function () {

      var res = {};
      for (var key in this.responses) {
        res[key] = this.extractResponse(key);
      }
      return res;
    };

    this.extractResponse = function (key) {
      return this.combineMessages(this.responses[key]);
    };

    this.replaceAttachmentTokens = function (attachments) {

      if (attachments && attachments.length) {
        for (let a = 0; a < attachments.length; a++) {
          for (let key in attachments[a]) {
            if (typeof (attachments[a][key]) == 'string') {
              attachments[a][key] = this.replaceTokens(attachments[a][key]);
            } else {
              attachments[a][key] = this.replaceAttachmentTokens(attachments[a][key]);
            }
          }
        }
      } else {
        for (let a in attachments) {
          if (typeof (attachments[a]) == 'string') {
            attachments[a] = this.replaceTokens(attachments[a]);
          } else {
            attachments[a] = this.replaceAttachmentTokens(attachments[a]);
          }
        }
      }

      return attachments;
    };


    this.replaceTokens = function (text) {

      var vars = {
        identity: this.task.bot.identity,
        responses: this.extractResponses(),
        origin: this.task.source_message,
        vars: this.vars,
      };

      var rendered = '';

      try {
        rendered = mustache.render(text, vars);
      } catch (err) {
        botkit.logger.error('Error in message template. Mustache failed with error: ', err);
        rendered = text;
      }

      return rendered;
    };

    this.stop = function (status) {
      this.handler = null;
      this.messages = [];
      this.status = status || 'stopped';
      botkit.logger.debug('Conversation is over with status ' + this.status);
      this.task.conversationEnded(this);
    };

    // was this conversation successful?
    // return true if it was completed
    // otherwise, return false
    // false could indicate a variety of failed states:
    // manually stopped, timed out, etc
    this.successful = function () {

      // if the conversation is still going, it can't be successful yet
      if (this.isActive()) {
        return false;
      }

      if (this.status == 'completed') {
        return true;
      } else {
        return false;
      }

    };

    this.cloneMessage = function (message) {
      // clone this object so as not to modify source
      var outbound = clone(message);

      if (typeof (message.text) == 'string') {
        outbound.text = this.replaceTokens(message.text);
      } else if (message.text) {
        outbound.text = this.replaceTokens(
          message.text[Math.floor(Math.random() * message.text.length)]
        );
      }

      if (outbound.attachments) {
        outbound.attachments = this.replaceAttachmentTokens(outbound.attachments);
      }

      if (outbound.attachment) {

        // pick one variation of the message text at random
        if (outbound.attachment.payload.text && typeof (outbound.attachment.payload.text) != 'string') {
          outbound.attachment.payload.text = this.replaceTokens(
            outbound.attachment.payload.text[
            Math.floor(Math.random() * outbound.attachment.payload.text.length)
            ]
          );
        }
        outbound.attachment = this.replaceAttachmentTokens([outbound.attachment])[0];
      }

      if (this.messages.length && !message.handler) {
        outbound.continue_typing = true;
      }

      if (typeof (message.attachments) == 'function') {
        outbound.attachments = message.attachments(this);
      }

      return outbound;
    };

    this.onTimeout = function (handler) {
      if (typeof (handler) == 'function') {
        this.timeOutHandler = handler;
      } else {
        botkit.logger.debug('Invalid timeout function passed to onTimeout');
      }
    };

    this.tick = function () {
      var now = new Date();

      if (this.isActive()) {
        if (this.processing) {
          // do nothing. The bot is waiting for async process to complete.
        } else if (this.handler) {
          // check timeout!
          // how long since task started?
          var duration = (now.getTime() - this.task.startTime.getTime());
          // how long since last active?
          var lastActive = (now.getTime() - this.lastActive.getTime());

          if (this.task.timeLimit && // has a timelimit
            (duration > this.task.timeLimit) && // timelimit is up
            (lastActive > this.task.timeLimit) // nobody has typed for 60 seconds at least
          ) {
            // if timeoutHandler is set then call it, otherwise follow the normal flow
            // this will not break others code, after the update
            if (this.timeOutHandler) {
              this.timeOutHandler(this);
            } else if (this.hasThread('on_timeout')) {
              this.status = 'ending';
              this.gotoThread('on_timeout');
            } else {
              this.stop('timeout');
            }
          }
          // otherwise do nothing
        } else {
          if (this.messages.length) {

            if (this.sent.length &&
              !this.sent[this.sent.length - 1].sent
            ) {
              return;
            }

            if (this.task.bot.botkit.config.require_delivery &&
              this.sent.length &&
              !this.sent[this.sent.length - 1].delivered
            ) {
              return;
            }

            if (typeof (this.messages[0].timestamp) == 'undefined' ||
              this.messages[0].timestamp <= now.getTime()) {
              var message = this.messages.shift();
              //console.log('HANDLING NEW MESSAGE',message);
              // make sure next message is delayed appropriately
              if (this.messages.length && this.messages[0].delay) {
                this.messages[0].timestamp = now.getTime() + this.messages[0].delay;
              }
              if (message.handler) {
                //console.log(">>>>>> SET HANDLER IN TICK");
                this.handler = message.handler;
              } else {
                this.handler = null;
                //console.log(">>>>>>> CLEARING HANDLER BECAUSE NO HANDLER NEEDED");
              }
              if (message.capture_options) {
                this.capture_options = message.capture_options;
              } else {
                this.capture_options = {};
              }

              this.lastActive = new Date();

              // is there any text?
              // or an attachment? (facebook)
              // or multiple attachments (slack)
              // if (message.text || message.attachments || message.attachment) {
              if (message) {

                var outbound = this.cloneMessage(message);
                var that = this;

                outbound.sent_timestamp = new Date().getTime();

                that.sent.push(outbound);
                that.transcript.push(outbound);

                this.task.bot.reply(this.source_message, outbound, function (err, sent_message) {
                  if (err) {
                    botkit.logger.error('An error occurred while sending a message: ', err);

                    // even though an error occured, set sent to true
                    // this will allow the conversation to keep going even if one message fails
                    // TODO: make a message that fails to send _resend_ at least once
                    that.sent[that.sent.length - 1].sent = true;
                    that.sent[that.sent.length - 1].api_response = err;

                  } else {

                    that.sent[that.sent.length - 1].sent = true;
                    that.sent[that.sent.length - 1].api_response = sent_message;

                    // if sending via slack's web api, there is no further confirmation
                    // so we can mark the message delivered
                    if (that.task.bot.type == 'slack' && sent_message && sent_message.ts) {
                      that.sent[that.sent.length - 1].delivered = true;
                    }

                    that.trigger('sent', [sent_message]);

                  }
                });
              }
              if (message.action) {
                if (typeof (message.action) == 'function') {
                  message.action(this);
                } else if (message.action == 'repeat') {
                  this.repeat();
                } else if (message.action == 'wait') {
                  this.silentRepeat();
                } else if (message.action == 'stop') {
                  this.stop();
                } else if (message.action == 'timeout') {
                  this.stop('timeout');
                } else if (this.threads[message.action]) {
                  this.gotoThread(message.action);
                }
              }
            } else {
              //console.log('Waiting to send next message...');
            }

            // end immediately instad of waiting til next tick.
            // if it hasn't already been ended by a message action!
            if (this.isActive() && !this.messages.length && !this.handler && !this.processing) {
              this.stop('completed');
            }

          } else if (this.sent.length) { // sent at least 1 message
            this.stop('completed');
          }
        }
      }
    };

    botkit.logger.debug('CREATED A CONVO FOR', this.source_message.user, this.source_message.channel);
    this.gotoThread('default');
  }
function Conversation ( task , message ) { this . messages = [ ] ; this . sent = [ ] ; this . transcript = [ ] ; this . context = { user : message . user , channel : message . channel , bot : task . bot , } ; this . events = { } ; this . vars = { } ; this . threads = { } ; this . thread = null ; this . status = 'new' ; this . task = task ; this . source_message = message ; this . handler = null ; this . responses = { } ; this . capture_options = { } ; this . startTime = new Date ( ) ; this . lastActive = new Date ( ) ; /** will be pointing to a callback which will be called after timeout,
     * conversation will be not be ended and should be taken care by callback
     */ this . timeOutHandler = null ; this . collectResponse = function ( key , value ) { this . responses [ key ] = value ; } ; this . capture = function ( response , cb ) { var that = this ; var capture_key = this . sent [ this . sent . length - 1 ] . text ; botkit . middleware . capture . run ( that . task . bot , response , that , function ( err , bot , response , convo ) { if ( response . text ) { response . text = response . text . trim ( ) ; } else { response . text = '' ; } if ( that . capture_options . key != undefined ) { capture_key = that . capture_options . key ; } // capture the question that was asked // if text is an array, get 1st if ( typeof ( that . sent [ that . sent . length - 1 ] . text ) == 'string' ) { response . question = that . sent [ that . sent . length - 1 ] . text ; } else if ( Array . isArray ( that . sent [ that . sent . length - 1 ] . text ) ) { response . question = that . sent [ that . sent . length - 1 ] . text [ 0 ] ; } else { response . question = '' ; } if ( that . capture_options . multiple ) { if ( ! that . responses [ capture_key ] ) { that . responses [ capture_key ] = [ ] ; } that . responses [ capture_key ] . push ( response ) ; } else { that . responses [ capture_key ] = response ; } if ( cb ) cb ( response ) ; } ) ; } ; this . handle = function ( message ) { var that = this ; this . lastActive = new Date ( ) ; this . transcript . push ( message ) ; botkit . logger . debug ( 'HANDLING MESSAGE IN CONVO' , message ) ; // do other stuff like call custom callbacks if ( this . handler ) { this . capture ( message , function ( message ) { // if the handler is a normal function, just execute it! // NOTE: anyone who passes in their own handler has to call // convo.next() to continue after completing whatever it is they want to do. if ( typeof ( that . handler ) == 'function' ) { that . handler ( message , that ) ; } else { // handle might be a mapping of keyword to callback. // lets see if the message matches any of the keywords let match , patterns = that . handler ; for ( let p = 0 ; p < patterns . length ; p ++ ) { if ( patterns [ p ] . pattern && botkit . hears_test ( [ patterns [ p ] . pattern ] , message ) ) { botkit . middleware . heard . run ( that . task . bot , message , function ( err , bot , message ) { patterns [ p ] . callback ( message , that ) ; } ) ; return ; } } // none of the messages matched! What do we do? // if a default exists, fire it! for ( let p = 0 ; p < patterns . length ; p ++ ) { if ( patterns [ p ] . default ) { botkit . middleware . heard . run ( that . task . bot , message , function ( err , bot , message ) { patterns [ p ] . callback ( message , that ) ; } ) ; return ; } } } } ) ; } else { // do nothing } } ; this . setVar = function ( field , value ) { if ( ! this . vars ) { this . vars = { } ; } this . vars [ field ] = value ; } ; this . activate = function ( ) { this . task . trigger ( 'conversationStarted' , [ this ] ) ; this . task . botkit . trigger ( 'conversationStarted' , [ this . task . bot , this ] ) ; this . status = 'active' ; } ; /**
     * active includes both ACTIVE and ENDING
     * in order to allow the timeout end scripts to play out
     **/ this . isActive = function ( ) { return ( this . status == 'active' || this . status == 'ending' ) ; } ; this . deactivate = function ( ) { this . status = 'inactive' ; } ; this . say = function ( message ) { this . addMessage ( message ) ; } ; this . sayFirst = function ( message ) { if ( typeof ( message ) == 'string' ) { message = { text : message , channel : this . source_message . channel , } ; } else { message . channel = this . source_message . channel ; } this . messages . unshift ( message ) ; } ; this . on = function ( event , cb ) { botkit . logger . debug ( 'Setting up a handler for' , event ) ; var events = event . split ( / \, / g ) ; for ( var e in events ) { if ( ! this . events [ events [ e ] ] ) { this . events [ events [ e ] ] = [ ] ; } this . events [ events [ e ] ] . push ( cb ) ; } return this ; } ; this . trigger = function ( event , data ) { if ( this . events [ event ] ) { for ( var e = 0 ; e < this . events [ event ] . length ; e ++ ) { var res = this . events [ event ] [ e ] . apply ( this , data ) ; if ( res === false ) { return ; } } } else { } } ; // proceed to the next message after waiting for an answer this . next = function ( ) { this . handler = null ; } ; this . repeat = function ( ) { if ( this . sent . length ) { this . messages . push ( this . sent [ this . sent . length - 1 ] ) ; } else { // console.log('TRIED TO REPEAT, NOTHING TO SAY'); } } ; this . silentRepeat = function ( ) { return ; } ; this . addQuestion = function ( message , cb , capture_options , thread ) { if ( typeof ( message ) == 'string' ) { message = { text : message , channel : this . source_message . channel } ; } else { message . channel = this . source_message . channel ; } if ( capture_options ) { message . capture_options = capture_options ; } message . handler = cb ; this . addMessage ( message , thread ) ; } ; this . ask = function ( message , cb , capture_options ) { this . addQuestion ( message , cb , capture_options , this . thread || 'default' ) ; } ; this . addMessage = function ( message , thread ) { if ( ! thread ) { thread = this . thread ; } if ( typeof ( message ) == 'string' ) { message = { text : message , channel : this . source_message . channel , } ; } else { message . channel = this . source_message . channel ; } if ( ! this . threads [ thread ] ) { this . threads [ thread ] = [ ] ; } this . threads [ thread ] . push ( message ) ; // this is the current topic, so add it here as well if ( this . thread == thread ) { this . messages . push ( message ) ; } } ; // how long should the bot wait while a user answers? this . setTimeout = function ( timeout ) { this . task . timeLimit = timeout ; } ; // For backwards compatibility, wrap gotoThread in its previous name this . changeTopic = function ( topic ) { this . gotoThread ( topic ) ; } ; this . hasThread = function ( thread ) { return ( this . threads [ thread ] != undefined ) ; } ; this . transitionTo = function ( thread , message ) { // add a new transition thread // add this new message to it // set that message action to execute the actual transition // then change threads to transition thread var num = 1 ; while ( this . hasThread ( 'transition_' + num ) ) { num ++ ; } var threadname = 'transition_' + num ; if ( typeof ( message ) == 'string' ) { message = { text : message , action : thread } ; } else { message . action = thread ; } this . addMessage ( message , threadname ) ; this . gotoThread ( threadname ) ; } ; this . beforeThread = function ( thread , callback ) { if ( ! this . before_hooks ) { this . before_hooks = { } ; } if ( ! this . before_hooks [ thread ] ) { this . before_hooks [ thread ] = [ ] ; } this . before_hooks [ thread ] . push ( callback ) ; } ; this . gotoThread = function ( thread ) { var that = this ; that . next_thread = thread ; that . processing = true ; var makeChange = function ( ) { if ( ! that . hasThread ( that . next_thread ) ) { if ( that . next_thread == 'default' ) { that . threads [ that . next_thread ] = [ ] ; } else { botkit . logger . debug ( 'WARN: gotoThread() to an invalid thread!' , thread ) ; that . stop ( 'unknown_thread' ) ; return ; } } that . thread = that . next_thread ; that . messages = that . threads [ that . next_thread ] . slice ( ) ; that . handler = null ; that . processing = false ; } ; if ( that . before_hooks && that . before_hooks [ that . next_thread ] ) { // call any beforeThread hooks in sequence async . eachSeries ( this . before_hooks [ that . next_thread ] , function ( before_hook , next ) { before_hook ( that , next ) ; } , function ( err ) { if ( ! err ) { makeChange ( ) ; } } ) ; } else { makeChange ( ) ; } } ; this . combineMessages = function ( messages ) { if ( ! messages ) { return '' ; } if ( Array . isArray ( messages ) && ! messages . length ) { return '' ; } if ( messages . length > 1 ) { var txt = [ ] ; var last_user = null ; var multi_users = false ; last_user = messages [ 0 ] . user ; for ( let x = 0 ; x < messages . length ; x ++ ) { if ( messages [ x ] . user != last_user ) { multi_users = true ; } } last_user = '' ; for ( let x = 0 ; x < messages . length ; x ++ ) { if ( multi_users && messages [ x ] . user != last_user ) { last_user = messages [ x ] . user ; if ( txt . length ) { txt . push ( '' ) ; } txt . push ( '<@' + messages [ x ] . user + '>:' ) ; } txt . push ( messages [ x ] . text ) ; } return txt . join ( '\n' ) ; } else { if ( messages . length ) { return messages [ 0 ] . text ; } else { return messages . text ; } } } ; this . getResponses = function ( ) { var res = { } ; for ( var key in this . responses ) { res [ key ] = { question : this . responses [ key ] . length ? this . responses [ key ] [ 0 ] . question : this . responses [ key ] . question , key : key , answer : this . extractResponse ( key ) , } ; } return res ; } ; this . getResponsesAsArray = function ( ) { var res = [ ] ; for ( var key in this . responses ) { res . push ( { question : this . responses [ key ] . length ? this . responses [ key ] [ 0 ] . question : this . responses [ key ] . question , key : key , answer : this . extractResponse ( key ) , } ) ; } return res ; } ; this . extractResponses = function ( ) { var res = { } ; for ( var key in this . responses ) { res [ key ] = this . extractResponse ( key ) ; } return res ; } ; this . extractResponse = function ( key ) { return this . combineMessages ( this . responses [ key ] ) ; } ; this . replaceAttachmentTokens = function ( attachments ) { if ( attachments && attachments . length ) { for ( let a = 0 ; a < attachments . length ; a ++ ) { for ( let key in attachments [ a ] ) { if ( typeof ( attachments [ a ] [ key ] ) == 'string' ) { attachments [ a ] [ key ] = this . replaceTokens ( attachments [ a ] [ key ] ) ; } else { attachments [ a ] [ key ] = this . replaceAttachmentTokens ( attachments [ a ] [ key ] ) ; } } } } else { for ( let a in attachments ) { if ( typeof ( attachments [ a ] ) == 'string' ) { attachments [ a ] = this . replaceTokens ( attachments [ a ] ) ; } else { attachments [ a ] = this . replaceAttachmentTokens ( attachments [ a ] ) ; } } } return attachments ; } ; this . replaceTokens = function ( text ) { var vars = { identity : this . task . bot . identity , responses : this . extractResponses ( ) , origin : this . task . source_message , vars : this . vars , } ; var rendered = '' ; try { rendered = mustache . render ( text , vars ) ; } catch ( err ) { botkit . logger . error ( 'Error in message template. Mustache failed with error: ' , err ) ; rendered = text ; } return rendered ; } ; this . stop = function ( status ) { this . handler = null ; this . messages = [ ] ; this . status = status || 'stopped' ; botkit . logger . debug ( 'Conversation is over with status ' + this . status ) ; this . task . conversationEnded ( this ) ; } ; // was this conversation successful? // return true if it was completed // otherwise, return false // false could indicate a variety of failed states: // manually stopped, timed out, etc this . successful = function ( ) { // if the conversation is still going, it can't be successful yet if ( this . isActive ( ) ) { return false ; } if ( this . status == 'completed' ) { return true ; } else { return false ; } } ; this . cloneMessage = function ( message ) { // clone this object so as not to modify source var outbound = clone ( message ) ; if ( typeof ( message . text ) == 'string' ) { outbound . text = this . replaceTokens ( message . text ) ; } else if ( message . text ) { outbound . text = this . replaceTokens ( message . text [ Math . floor ( Math . random ( ) * message . text . length ) ] ) ; } if ( outbound . attachments ) { outbound . attachments = this . replaceAttachmentTokens ( outbound . attachments ) ; } if ( outbound . attachment ) { // pick one variation of the message text at random if ( outbound . attachment . payload . text && typeof ( outbound . attachment . payload . text ) != 'string' ) { outbound . attachment . payload . text = this . replaceTokens ( outbound . attachment . payload . text [ Math . floor ( Math . random ( ) * outbound . attachment . payload . text . length ) ] ) ; } outbound . attachment = this . replaceAttachmentTokens ( [ outbound . attachment ] ) [ 0 ] ; } if ( this . messages . length && ! message . handler ) { outbound . continue_typing = true ; } if ( typeof ( message . attachments ) == 'function' ) { outbound . attachments = message . attachments ( this ) ; } return outbound ; } ; this . onTimeout = function ( handler ) { if ( typeof ( handler ) == 'function' ) { this . timeOutHandler = handler ; } else { botkit . logger . debug ( 'Invalid timeout function passed to onTimeout' ) ; } } ; this . tick = function ( ) { var now = new Date ( ) ; if ( this . isActive ( ) ) { if ( this . processing ) { // do nothing. The bot is waiting for async process to complete. } else if ( this . handler ) { // check timeout! // how long since task started? var duration = ( now . getTime ( ) - this . task . startTime . getTime ( ) ) ; // how long since last active? var lastActive = ( now . getTime ( ) - this . lastActive . getTime ( ) ) ; if ( this . task . timeLimit && // has a timelimit ( duration > this . task . timeLimit ) && // timelimit is up ( lastActive > this . task . timeLimit ) // nobody has typed for 60 seconds at least ) { // if timeoutHandler is set then call it, otherwise follow the normal flow // this will not break others code, after the update if ( this . timeOutHandler ) { this . timeOutHandler ( this ) ; } else if ( this . hasThread ( 'on_timeout' ) ) { this . status = 'ending' ; this . gotoThread ( 'on_timeout' ) ; } else { this . stop ( 'timeout' ) ; } } // otherwise do nothing } else { if ( this . messages . length ) { if ( this . sent . length && ! this . sent [ this . sent . length - 1 ] . sent ) { return ; } if ( this . task . bot . botkit . config . require_delivery && this . sent . length && ! this . sent [ this . sent . length - 1 ] . delivered ) { return ; } if ( typeof ( this . messages [ 0 ] . timestamp ) == 'undefined' || this . messages [ 0 ] . timestamp <= now . getTime ( ) ) { var message = this . messages . shift ( ) ; //console.log('HANDLING NEW MESSAGE',message); // make sure next message is delayed appropriately if ( this . messages . length && this . messages [ 0 ] . delay ) { this . messages [ 0 ] . timestamp = now . getTime ( ) + this . messages [ 0 ] . delay ; } if ( message . handler ) { //console.log(">>>>>> SET HANDLER IN TICK"); this . handler = message . handler ; } else { this . handler = null ; //console.log(">>>>>>> CLEARING HANDLER BECAUSE NO HANDLER NEEDED"); } if ( message . capture_options ) { this . capture_options = message . capture_options ; } else { this . capture_options = { } ; } this . lastActive = new Date ( ) ; // is there any text? // or an attachment? (facebook) // or multiple attachments (slack) // if (message.text || message.attachments || message.attachment) { if ( message ) { var outbound = this . cloneMessage ( message ) ; var that = this ; outbound . sent_timestamp = new Date ( ) . getTime ( ) ; that . sent . push ( outbound ) ; that . transcript . push ( outbound ) ; this . task . bot . reply ( this . source_message , outbound , function ( err , sent_message ) { if ( err ) { botkit . logger . error ( 'An error occurred while sending a message: ' , err ) ; // even though an error occured, set sent to true // this will allow the conversation to keep going even if one message fails // TODO: make a message that fails to send _resend_ at least once that . sent [ that . sent . length - 1 ] . sent = true ; that . sent [ that . sent . length - 1 ] . api_response = err ; } else { that . sent [ that . sent . length - 1 ] . sent = true ; that . sent [ that . sent . length - 1 ] . api_response = sent_message ; // if sending via slack's web api, there is no further confirmation // so we can mark the message delivered if ( that . task . bot . type == 'slack' && sent_message && sent_message . ts ) { that . sent [ that . sent . length - 1 ] . delivered = true ; } that . trigger ( 'sent' , [ sent_message ] ) ; } } ) ; } if ( message . action ) { if ( typeof ( message . action ) == 'function' ) { message . action ( this ) ; } else if ( message . action == 'repeat' ) { this . repeat ( ) ; } else if ( message . action == 'wait' ) { this . silentRepeat ( ) ; } else if ( message . action == 'stop' ) { this . stop ( ) ; } else if ( message . action == 'timeout' ) { this . stop ( 'timeout' ) ; } else if ( this . threads [ message . action ] ) { this . gotoThread ( message . action ) ; } } } else { //console.log('Waiting to send next message...'); } // end immediately instad of waiting til next tick. // if it hasn't already been ended by a message action! if ( this . isActive ( ) && ! this . messages . length && ! this . handler && ! this . processing ) { this . stop ( 'completed' ) ; } } else if ( this . sent . length ) { // sent at least 1 message this . stop ( 'completed' ) ; } } } } ; botkit . logger . debug ( 'CREATED A CONVO FOR' , this . source_message . user , this . source_message . channel ) ; this . gotoThread ( 'default' ) ; }
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~