bot.js

'use strict';

const Alias = require('./alias');
const bindLogger = require('./util/bind-logger');
const botkit = require('botkit');
const DataManager = require('./data-manager');
const defaults = require('lodash/defaults');
const Listener = require('./listener');
const pick = require('lodash/pick');
const Response = require('./response');

/**
 * Class representing a Slack bot.
 *
 * @example <caption>Create a Slack bot, passing in options</caption>
 * const bot = new Bot({
 *     name: 'ExampleBot',
 *     slackToken: process.env.SLACK_TOKEN
 * });
 *
 * @example <caption>Start the bot</caption>
 * bot.connect()
 *     .then(() => {
 *         console.log('Bot started');
 *     })
 *     .catch((error) => {
 *         console.error(`Bot failed to start: ${error.message}`);
 *     });
 */
class Bot {

	/**
	 * Create a Slack bot.
	 * @param {Object} options - The bot options.
	 * @param {String} options.name - The bot's name.
	 * @param {String} options.slackToken - The token to use when connecting to Slack.
	 * @param {Boolean} [options.includeBotkitLogs=false] - Whether to include Botkit logs in the output.
	 * @param {Boolean} [options.includeBotkitDebugLogs=false] - Whether to include Botkit debug logs in the output.
	 * @param {Object} [options.log] - A logger which has `error` and `info` methods.
	 * @throws {TypeError} Will throw if the name option is not set.
	 * @throws {TypeError} Will throw if the slackToken option is not set.
	 */
	constructor(options) {

		// Default the passed in options so we know we've got
		// everything that we need to start up
		this.options = defaults({}, options, Bot.defaults);

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

		// Validate the slackToken option
		this.slackToken = this.options.slackToken;
		if (!this.slackToken) {
			throw new TypeError('Bot slackToken must be set');
		}

		this.log = bindLogger(this.options.log, `${this.name}:`);
		this.alias = {};
		this.listeners = [];

		this.botkit = {};
		this.botkit.controller = botkit.slackbot({
			/* eslint-disable camelcase */
			disable_startup_messages: true,
			/* eslint-enable camelcase */
			logger: {
				log: (type, ...args) => {
					if (this.options.includeBotkitLogs) {
						if (type !== 'debug' || this.options.includeBotkitDebugLogs) {
							return this.log.info(`Botkit [${type}]:`, ...args);
						}
					}
				}
			}
		});
		this.botkit.bot = this.botkit.controller.spawn({
			token: this.options.slackToken
		});

		const dataManager = DataManager.create();
		dataManager.registerToBot(this);

		this.log.info(`✔︎ initialization complete`);
	}

	/**
	 * Extend the bot's functionality.
	 * @param {(Alias|Function|Listener|String)} extension - The extension.
	 * If this is an {@link Alias} or {@link Listener}, it will be registered to the bot.
	 * If this is a function, it will be called with the bot as a first argument.
	 * If this is a string, it will be required as a module and run through this method again.
	 * @returns {*} The return value of the extension call.
	 * @throws {TypeError} Will throw if the extension type is invalid.
	 */
	use(extension) {
		if (typeof extension === 'string') {
			return this.use(require(extension));
		}
		if (extension instanceof Alias) {
			return extension.registerToBot(this);
		}
		if (extension instanceof Listener) {
			return extension.registerToBot(this);
		}
		if (typeof extension === 'function') {
			return extension(this);
		}
		throw new TypeError(`Bot extension cannot be of type "${typeof extension}"`);
	}

	/**
	 * Respond to an incoming message. This method is a shortcut to creating
	 * a new {@link Response} object and setting the incoming message.
	 * @param {Object} incomingMessage - The Slack message to respond to.
	 * @returns {Response} The created response.
	 */
	replyTo(incomingMessage) {
		return Response.create(this).to(incomingMessage);
	}

	/**
	 * Connect to Slack.
	 * @returns {Promise} A promise which resolves when a connection is made.
	 */
	connect() {
		return new Promise((resolve, reject) => {
			this.botkit.bot.startRTM(error => {
				if (error) {
					return reject(new Error(`Bot could not connect to Slack: ${error}`));
				}
				this.log.info(`✔︎ connected to Slack`);
				resolve();
			});
		});
	}

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

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

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

}

/**
 * The default options used when constructing a bot.
 * @static
 */
Bot.defaults = {
	includeBotkitLogs: false,
	includeBotkitDebugLogs: false,
	log: console
};

module.exports = Bot;