SockDrawer/SockBot

View on GitHub
providers/nodebb/chat.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

const utils = require('../../lib/utils');

/**
 * Create a ChatRoom class and bind it to a forum instance
 *
 * @param {Provider} forum A forum instance to bind to constructed ChatRoom class
 * @returns {User} A ChatRoomPo class bound to the provided `forum` instance
 */
exports.bindChat = function bindChat(forum) {

    /**
     * Send a message to the chatroom
     *
     * @public
     *
     * @param {number} roomId Chatroom to speak to
     * @param {string} content Message to send to the chatroom
     * @returns {Promise} Resolves when message has been sent
     */
    function sendChat(roomId, content) {
        return retryAction(() => forum._emit('modules.chats.send', {
            roomId: roomId,
            message: content
        }), 5);
    }

    /**
     * Message Class
     *
     * Represents a message in a chatroom
     *
     * @public
     */
    class Message {
        /**
         * Construct a Message object from payload
         *
         * This constructor is intended to be private use only, if you need to construct a chat message from payload
         * data use `Message.parse()` instead
         *
         * @private
         * @class
         *
         * @param {*} payload Payload to construct the Message object out of
         */
        constructor(payload) {
            payload = utils.parseJSON(payload);
            const values = {
                markup: payload.content,
                content: utils.htmlToRaw(payload.content),
                id: payload.messageId,
                room: payload.roomId,
                from: forum.User.parse(payload.fromUser),
                sent: new Date(payload.timestamp),
                self: payload.self === 1
            };
            utils.mapSet(this, values);
        }

        /**
         * Chat message id
         *
         * @public
         *
         * @returns {number} Id of the chat message
         */
        get id() {
            return utils.mapGet(this, 'id');
        }

        /**
         * Id of the chatroom this message belongs to
         *
         * @public
         *
         * @returns {number} Id of the ChatRoom this message belongs to
         */
        get room() {
            return utils.mapGet(this, 'room');
        }

        /**
         * User who sent this message
         *
         * @public
         *
         * @returns {User} User who authored this chat message
         */
        get from() {
            return utils.mapGet(this, 'from');
        }

        /**
         * Identify if this message was created by current user
         *
         * @public
         *
         * @returns {boolean} True if message was sent by current user
         */
        get self() {
            return utils.mapGet(this, 'self');
        }

        /**
         * Text content of message
         *
         * @public
         *
         * @returns {string} Content of the message with formatting and quotes removed
         */
        get content() {
            return utils.mapGet(this, 'content');
        }

        /**
         * DateTime the message was sent
         *
         * @public
         *
         * @returns {Date} datetime the message was sent
         */
        get sent() {
            return utils.mapGet(this, 'sent');
        }

        /**
         * Message markup
         *
         * @public
         *
         * @returns {Promise<string>} Resolves to the HTML markup of the chat message
         */
        markup() {
            return Promise.resolve(utils.mapGet(this, 'markup'));
        }

        /**
         * Reply to the chat message
         *
         * @public
         *
         * @param {string} content Message to reply with
         * @returns {Promise} Resolves once message has been sent
         */
        reply(content) {
            return sendChat(this.room, content).then(() => this);
        }

        /**
         * Parse a Message from a given payload
         *
         * @public
         *
         * @param {string|object} payload Data to parse as a Message
         * @returns {Message} parsed Message
         */
        static parse(payload) {
            if (!payload) {
                throw new Error('E_CHATMESSAGE_NOT_FOUND');
            }
            return new Message(payload);
        }
    }

    /**
     * ChatRoom Class
     *
     * Represents a chat room
     *
     * @public
     *
     */
    class ChatRoom {

        /**
         * Construct a ChatroomObject from payload
         *
         * This constructor is intended to be private use only, if you need to construct a chatroom from payload
         * data use `ChatRoom.parse()` instead
         *
         * @private
         * @class
         *
         * @param {*} payload Payload to construct the ChatRoom object out of
         */
        constructor(payload) {
            payload = utils.parseJSON(payload);
            const values = {
                id: payload.roomId,
                owner: payload.owner,
                name: payload.roomName,
                users: payload.users || []
            };
            utils.mapSet(this, values);
        }

        /**
         * Get the chatroom id
         *
         * @public
         *
         * @returns {number} Id of the chatroom
         *
         */
        get id() {
            return utils.mapGet(this, 'id');
        }

        /**
         * Get the chatroom name
         *
         * @public
         *
         * @returns {string} Name of the chatroom
         */
        get name() {
            return utils.mapGet(this, 'name');
        }

        /**
         * Get the users in the chatroom
         *
         * @public
         *
         * @returns {User[]} The users that were in teh chatroom when the room was retrieved
         */
        get users() {
            return utils.mapGet(this, 'users').map((user) => forum.User.parse(user));
        }

        /**
         * Get the number of users in the chatroom
         *
         * @public
         *
         * @returns {number} Number of users in the chatroom
         */
        get participants() {
            return utils.mapGet(this, 'users').length;
        }

        /**
         * Get the owner of the chatroom
         *
         * @public
         *
         * @returns {User} Owning user for the chatroom
         */
        get owner() {
            const ownerId = utils.mapGet(this, 'owner');
            const owner = utils.mapGet(this, 'users').filter((user) => user.uid === ownerId)[0];
            return forum.User.parse(owner);
        }

        /**
         * Retrieve the weblink for the Chatroom
         *
         * @public
         *
         * @returns {Promise<string>} Resolves to the URL web link to the chatroom
         */
        url() {
            return Promise.resolve(`${forum.url}/chats/${this.id}`);
        }

        /**
         * Send a message to the chatroom
         *
         * @public
         *
         * @param {string} content Message to send to the chatroom
         * @returns {Promise} Resolves when message has been sent
         */
        send(content) {
            return sendChat(this.id, content).then(() => this);
        }

        /**
         * Apply an operation to a list of users
         *
         * @private
         *
         * @param {string} action Action to apply
         * @param {User|User[]} users List of users to apply action to
         * @returns {Promise} resolves when action has been applied to all users
         */
        _applyToUsers(action, users) {
            if (!Array.isArray(users)) {
                users = [users];
            }
            return Promise.all(
                users.map((user) => forum._emit(action, {
                    roomId: this.id,
                    username: user.username
                })));
        }

        /**
         * Add a list of users to the chatroom
         *
         * @public
         *
         * @param {User|User[]} users User or Users to add to the chatroom
         * @returns {Promise} Resolves when all users have been added to the chatroom
         */
        addUsers(users) {
            return this._applyToUsers('modules.chats.addUserToRoom', users).then(() => this);
        }

        /**
         * Remove a list of users from the chatroom
         *
         * @public
         *
         * @param {User|User[]} users User or Users to remove from the chatroom
         * @returns {Promsie} Resos when users have been removed from the chatroom
         */
        removeUsers(users) {
            return this._applyToUsers('modules.chats.removeUserFromRoom', users).then(() => this);
        }

        /**
         * Leave the chatroom
         *
         * This will remove current user from the chat.
         *
         * @public
         *
         * @returns {Promise} Resolves when chatroom has been left
         */
        leave() {
            return forum._emit('modules.chats.leave', this.id).then(() => this);
        }

        /**
         * Rename the chat room
         *
         * @public
         *
         * @param {string} newName Name to set the chatroom to
         * @returns {Promise} Resolves when rename is complete
         */
        rename(newName) {
            return forum._emit('modules.chats.renameRoom', {
                roomId: this.id,
                newName: newName
            }).then(() => this);
        }

        /**
         * Create a new chatroom, add a list of users to it and send a message.
         *
         * @public
         *
         * @param {User|User[]} users User or users to add to the chatroom
         * @param {string} message Message to send to the new chat room
         * @param {string} [title] Optional: Set the title of the chat message to this value
         * @returns {Promise} Resolves once message has been sent
         */
        static create(users, message, title) {
            if (!Array.isArray(users)) {
                users = [users];
            }
            const rootUser = users.shift();
            const payload = {
                touid: rootUser.id
            };
            return retryAction(() => forum._emit('modules.chats.newRoom', payload), 5)
                .then((roomId) => ChatRoom.get(roomId))
                .then((chat) => chat.addUsers(users))
                .then((chat) => chat.send(message))
                .then((chat) => {
                    if (title) {
                        return chat.rename(title);
                    }
                    return Promise.resolve(chat);
                });
        }

        /**
         * Activate chat features. newly received chat messages will be processed
         *
         * @public
         */
        static activate() {
            forum.socket.on('event:chats.receive', handleChat);
        }

        /**
         * Deactivate the Chat features. This will stop new chat messages from being processed
         *
         * @public
         */
        static deactivate() {
            forum.socket.off('event:chats.receive', handleChat);
        }

        /**
         * Retrieve a ChatRoom by a given ID
         *
         * @public
         *
         * @param {number} roomId Id of the chatroom to retrieve
         * @returns {Promise<ChatRoom>} Resolves to the chatroom requested
         */
        static get(roomId) {
            return forum._emit('modules.chats.loadRoom', {
                roomId: roomId
            }).then((data) => ChatRoom.parse(data));
        }

        /**
         * Parse a Chatroom object from payload
         *
         * @public
         *
         * @param {string|object} payload ChatRoom Payload
         * @returns {ChatRoom} Parsed Chatroom
         */
        static parse(payload) {
            if (!payload) {
                throw new Error('E_CHATROOM_NOT_FOUND');
            }
            return new ChatRoom(payload);
        }
    }
    ChatRoom.Message = Message;

    ChatRoom.retryDelay = 250;

    /**
     * Handle Chat events from websocket
     *
     * @private
     *
     * @param {*} payload websocket event payload
     * @returns {Promise} resolves when processing has been completed for event
     */
    function handleChat(payload) {
        if (!payload.message) {
            return Promise.reject(new Error('Event payload did not include chat message'));
        }
        const message = ChatRoom.Message.parse(payload.message);

        forum.emit('chatMessage', message);
        const ids = {
            post: -1,
            topic: -1,
            user: message.from.id,
            pm: message.room,
            chat: message.id
        };
        return forum.Commands.get(ids, message.content, (content) => message.reply(content))
            .then((command) => command.execute());
    }

    /**
     * @param {function} fn Promise returning function to possibly retry
     * @param {number} trials Number or times to retry
     * @returns {Promise} Resolves when
     */
    function retryAction(fn, trials) {
        return new Promise((resolve, reject) => {
            fn().then(resolve, (err) => {
                if (trials > 1 && err.message === '[[error:too-many-messages]]') {
                    // eslint-disable-once no-use-before-define
                    setTimeout(() => retryAction(fn, trials - 1).then(resolve, reject), ChatRoom.retryDelay);
                } else {
                    reject(err);
                }
            });
        });
    }

    return ChatRoom;
};