listener.js

'use strict';

const bindLogger = require('./util/bind-logger');
const defaults = require('lodash/defaults');
const pick = require('lodash/pick');
const Responder = require('./responder');
let Bot; // Not set here to get around circular dependency

/**
 * Class representing a bot listener.
 */
class Listener {

	/**
	 * Create a bot listener.
	 * @param {Object} options - The listener options.
	 * @param {String} options.name - The listener name, used in documentation.
	 * @param {(Array|RegExp|String)} [options.trigger] - The patterns which trigger the listener as
	 * a string, regular expression, or an array of them. When not defined, all messages will
	 * trigger the handler.
	 * @param {(Function|Responder)} options.handler - The handler for the listener.
	 * @throws {TypeError} Will throw if the name option is not set.
	 * @throws {TypeError} Will throw if the trigger option is invalid.
	 * @throws {TypeError} Will throw if the handler option is not set.
	 */
	constructor(options) {
		if (this.constructor === Listener) {
			throw new Error('You cannot create an instance of Listener, it is designed to be extended');
		}

		this.options = defaults({}, options, Listener.defaults);

		// Validate the name option
		this.name = this.options.name;
		if (!this.name) {
			throw new TypeError(`${this.constructor.name} name must be set`);
		}

		// Validate the trigger option
		this.trigger = this.options.trigger;
		if (typeof this.trigger !== 'string' && !(this.trigger instanceof RegExp) && !Array.isArray(this.trigger)) {
			throw new TypeError(`${this.constructor.name} trigger must be a string, array, or regular expression`);
		}
		this.triggerTypes = null;

		// Validate the handler option
		this.handler = this.options.handler;
		if (typeof this.handler !== 'function' && !(this.handler instanceof Responder)) {
			throw new TypeError(`${this.constructor.name} handler must be a function or Responder instance`);
		}

	}

	/**
	 * Register the listener to a bot.
	 * @access private
	 * @param {Bot} bot - The bot to register to.
	 * @returns {Listener} The calling listener instance.
	 * @throws {TypeError} Will throw if bot is not an instance of Bot.
	 */
	registerToBot(bot) {

		// NOTE: Bot is required here to get around a
		// circular dependency when the module is loaded
		Bot = Bot || require('./bot');

		if (!(bot instanceof Bot)) {
			throw new TypeError('Expected an instance of Bot');
		}

		this.bot = bot;
		bot.listeners.push(this);

		if (this.handler instanceof Responder) {
			this.handler.registerToBot(bot);
		}

		this.log = bindLogger(bot.log, `${this.constructor.name} (${this.name}):`);
		this.log.info('Registered to bot');

		bot.botkit.controller.hears(
			this.trigger,
			this.triggerTypes,
			this.handleMessage.bind(this)
		);

		return this;
	}

	/**
	 * Handle a Slack message.
	 * @access private
	 * @param {Object} botkitBot - A botkit bot that can be used to respond to the message.
	 * @param {Object} message - A Slack message to handle.
	 * @returns {Undefined} No return value.
	 */
	handleMessage(botkitBot, message) {
		if (this.handler instanceof Responder) {
			this.handler.respond(message);
		} else {
			this.handler(message);
		}
	}

	/**
	 * Get the listener as a string, for use in implicit type conversion.
	 * @access private
	 * @returns {String} The string representation.
	 */
	valueOf() {
		return `[object ${this.constructor.name}]`;
	}

	/**
	 * Get the listener as a plain object, for use in JSON conversion.
	 * @access private
	 * @returns {Object} The listener as a plain object.
	 */
	toJSON() {
		return pick(this, [
			'name'
		]);
	}

	/**
	 * Get console-friendly representation of the listener.
	 * @access private
	 * @returns {String} The console-friendly representation.
	 */
	inspect() {
		return `${this.constructor.name} { '${this.name}' }`;
	}

}

/**
 * The default options used when constructing a listener.
 * @static
 */
Listener.defaults = {};

module.exports = Listener;