alias.js

'use strict';

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

/**
 * Class representing a bot alias. Aliases are used to identify the
 * bot as a different user in Slack by setting a custom name and avatar.
 *
 * @example <caption>Create and register an alias</caption>
 * bot.use(new Bot.Alias({
 *     name: 'DiceBot',
 *     avatar: 'game_die' // An emoji name or image URL
 * }));
 *
 * @example <caption>Use an alias as part of a {@link Responder}</caption>
 * bot.use(new Bot.Listener.Command({
 *     name: 'dice roller',
 *     trigger: 'roll a die',
 *     handler: new Bot.Responder.RandomMessage({
 *         alias: 'DiceBot',
 *         messages: [
 *             '1', '2', '3', '4', '5', '6'
 *         ]
 *     })
 * }));
 *
 * @example <caption>Use an alias as part of a handler function</caption>
 * bot.use(new Bot.Listener.Ambient({
 *     name: 'hunger listener',
 *     trigger: /i'?m hungry/i,
 *     handler: async message => {
 *         await bot
 *             .replyTo(message)
 *             .as('HungerBot')
 *             .with(`Hello hungry, I'm ${bot.name}`);
 *     }
 * }));
 */
class Alias {

	/**
	 * Create a bot alias.
	 * @param {Object} options - The alias options.
	 * @param {String} options.name - The name to give the bot when using this alias.
	 * @param {String} [options.avatar] - Either an emoji name or URL of an image to use as a bot avatar.
	 * @throws {TypeError} Will throw if the name option is not set.
	 * @throws {TypeError} Will throw if the avatar option is invalid.
	 */
	constructor(options) {
		this.options = defaults({}, options, Alias.defaults);

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

		// Validate the avatar option
		const avatarMatch = this.options.avatar.match(/^(https?:\/\/.+)|(:?([^:]+):?)$/i);
		if (avatarMatch && avatarMatch[1]) {
			this.avatar = avatarMatch[1];
			this.avatarType = 'url';
		} else if (avatarMatch && avatarMatch[3]) {
			this.avatar = `:${avatarMatch[3]}:`;
			this.avatarType = 'emoji';
		} else {
			throw new TypeError('Alias avatar must be a URL or emoji name');
		}
	}

	/**
	 * Compose a Slack message which includes the username and icon properties for this alias.
	 * @returns {Object} The composed Slack message.
	 */
	composeMessage() {
		return {
			username: this.name,
			[`icon_${this.avatarType}`]: this.avatar
		};
	}

	/**
	 * Register the alias to a bot.
	 * @access private
	 * @param {Bot} bot - The bot to register to.
	 * @returns {Alias} The calling alias 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.alias[this.name] = this;

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

		return this;
	}

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

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

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

	/**
	 * Create a bot alias (see {@link Alias} for parameters).
	 * @returns {Alias} The new alias.
	 */
	static create(...args) {
		return new this(...args);
	}

}

/**
 * The default options used when constructing an alias.
 * @static
 */
Alias.defaults = {
	avatar: ':grey_question:'
};

module.exports = Alias;