RocketChat/Rocket.Chat

View on GitHub
apps/meteor/app/slackbridge/server/SlackAdapter.js

Summary

Maintainability
F
2 wks
Test Coverage
import http from 'http';
import https from 'https';
import url from 'url';

import { Message } from '@rocket.chat/core-services';
import { Messages, Rooms, Users, ReadReceipts } from '@rocket.chat/models';
import { App as SlackApp } from '@slack/bolt';
import { RTMClient } from '@slack/rtm-api';
import { Meteor } from 'meteor/meteor';

import { saveRoomName, saveRoomTopic } from '../../channel-settings/server';
import { FileUpload } from '../../file-upload/server';
import { addUserToRoom } from '../../lib/server/functions/addUserToRoom';
import { archiveRoom } from '../../lib/server/functions/archiveRoom';
import { deleteMessage } from '../../lib/server/functions/deleteMessage';
import { removeUserFromRoom } from '../../lib/server/functions/removeUserFromRoom';
import { sendMessage } from '../../lib/server/functions/sendMessage';
import { unarchiveRoom } from '../../lib/server/functions/unarchiveRoom';
import { updateMessage } from '../../lib/server/functions/updateMessage';
import { executeSetReaction } from '../../reactions/server/setReaction';
import { settings } from '../../settings/server';
import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL';
import { SlackAPI } from './SlackAPI';
import { slackLogger } from './logger';

export default class SlackAdapter {
    constructor(slackBridge) {
        slackLogger.debug('constructor');
        this.slackBridge = slackBridge;
        this.rtm = {}; // slack-client Real Time Messaging API
        this.apiToken = {}; // Slack API Token passed in via Connect
        this.slackApp = {};
        this.appCredential = {};
        // On Slack, a rocket integration bot will be added to slack channels, this is the list of those channels, key is Rocket Ch ID
        this.slackChannelRocketBotMembershipMap = new Map(); // Key=RocketChannelID, Value=SlackChannel
        this.rocket = {};
        this.messagesBeingSent = [];
        this.slackBotId = false;

        this.slackAPI = {};
    }

    async connect({ apiToken, appCredential }) {
        try {
            const connectResult = await (appCredential ? this.connectApp(appCredential) : this.connectLegacy(apiToken));

            if (connectResult) {
                slackLogger.info('Connected to Slack');
                slackLogger.debug('Slack connection result: ', connectResult);
                Meteor.startup(async () => {
                    try {
                        await this.populateMembershipChannelMap(); // If run outside of Meteor.startup, HTTP is not defined
                    } catch (err) {
                        slackLogger.error({ msg: 'Error attempting to connect to Slack', err });
                        if (err.data.error === 'invalid_auth') {
                            slackLogger.error('The provided token is invalid');
                        }
                        this.slackBridge.disconnect();
                    }
                });
            }
        } catch (err) {
            slackLogger.error({ msg: 'Error attempting to connect to Slack', err });
            this.slackBridge.disconnect();
        }
    }

    /**
     * Connect to the remote Slack server using the passed in app credential and register for Slack events.
     * @typedef {Object} AppCredential
     * @property {string} botToken
     * @property {string} appToken
     * @property {string} signingSecret
     * @param {AppCredential} appCredential
     */
    async connectApp(appCredential) {
        this.appCredential = appCredential;

        // Invalid app credentials causes unhandled errors
        if (!(await SlackAPI.verifyAppCredentials(appCredential))) {
            throw new Error('Invalid app credentials (botToken or appToken) for the slack app');
        }
        this.slackAPI = new SlackAPI(this.appCredential.botToken);

        this.slackApp = new SlackApp({
            appToken: this.appCredential.appToken,
            signingSecret: this.appCredential.signingSecret,
            token: this.appCredential.botToken,
            socketMode: true,
        });

        this.registerForEvents();

        const connectionResult = await this.slackApp.start();

        return connectionResult;
    }

    /**
     * Connect to the remote Slack server using the passed in token API and register for Slack events.
     * @param apiToken
     * @deprecated
     */
    async connectLegacy(apiToken) {
        this.apiToken = apiToken;

        // Invalid apiToken causes unhandled errors
        if (!(await SlackAPI.verifyToken(apiToken))) {
            throw new Error('Invalid ApiToken for the slack legacy bot integration');
        }

        if (RTMClient != null) {
            RTMClient.disconnect;
        }
        this.slackAPI = new SlackAPI(this.apiToken);
        this.rtm = new RTMClient(this.apiToken);

        this.registerForEventsLegacy();

        const connectionResult = await this.rtm.start();

        return connectionResult;
    }

    /**
     * Unregister for slack events and disconnect from Slack
     */
    async disconnect() {
        if (this.rtm.connected && this.rtm.disconnect) {
            await this.rtm.disconnect();
        } else if (this.slackApp.stop) {
            await this.slackApp.stop();
        }
    }

    setRocket(rocket) {
        this.rocket = rocket;
    }

    registerForEvents() {
        /**
         * message: {
         * "client_msg_id": "caab144d-41e7-47cc-87fa-af5d50c02784",
         * "type": "message",
         * "text": "heyyyyy",
         * "user": "U060WD4QW81",
         * "ts": "1697054782.214569",
         * "blocks": [],
         * "team": "T060383CUDV",
         * "channel": "C060HSLQPCN",
         * "event_ts": "1697054782.214569",
         * "channel_type": "channel"
         * }
         */
        this.slackApp.message(async ({ message }) => {
            slackLogger.debug('OnSlackEvent-MESSAGE: ', message);
            if (message) {
                try {
                    await this.onMessage(message);
                } catch (err) {
                    slackLogger.error({ msg: 'Unhandled error onMessage', err });
                }
            }
        });

        /**
         * Event fired when a message is reacted in a channel or group app is added in
         * event: {
         * "type": "reaction_added",
         * "user": "U060WD4QW81",
         * "reaction": "telephone_receiver",
         * "item": {
         *   "type": "message",
         *   "channel": "C06196XMUMN",
         *   "ts": "1697037020.309679"
         * },
         * "item_user": "U060WD4QW81",
         * "event_ts": "1697037219.001600"
         * }
         */
        this.slackApp.event('reaction_added', async ({ event }) => {
            slackLogger.debug('OnSlackEvent-REACTION_ADDED: ', event);
            try {
                slackLogger.error({ event });
                await this.onReactionAdded(event);
            } catch (err) {
                slackLogger.error({ msg: 'Unhandled error onReactionAdded', err });
            }
        });

        /**
         * Event fired when a reaction is removed from a message in a channel or group app is added in.
         * event: {
         * "type": "reaction_removed",
         * "user": "U060WD4QW81",
         * "reaction": "raised_hands",
         * "item": {
         *   "type": "message",
         *   "channel": "C06196XMUMN",
         *   "ts": "1697028997.057629"
         * },
         * "item_user": "U060WD4QW81",
         * "event_ts": "1697029220.000600"
         * }
         */
        this.slackApp.event('reaction_removed', async ({ event }) => {
            slackLogger.debug('OnSlackEvent-REACTION_REMOVED: ', event);
            try {
                await this.onReactionRemoved(event);
            } catch (err) {
                slackLogger.error({ msg: 'Unhandled error onReactionRemoved', err });
            }
        });

        /**
         * Event fired when a members joins a channel
         * event: {
         * "type": "member_joined_channel",
         * "user": "U06039U8WK1",
         * "channel": "C060HT033E2",
         * "channel_type": "C",
         * "team": "T060383CUDV",
         * "inviter": "U060WD4QW81",
         * "event_ts": "1697042377.000800"
         * }
         */
        this.slackApp.event('member_joined_channel', async ({ event, context }) => {
            slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', event);
            try {
                await this.processMemberJoinChannel(event, context);
            } catch (err) {
                slackLogger.error({ msg: 'Unhandled error onChannelLeft', err });
            }
        });

        this.slackApp.event('channel_left', async ({ event }) => {
            slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', event);
            try {
                this.onChannelLeft(event);
            } catch (err) {
                slackLogger.error({ msg: 'Unhandled error onChannelLeft', err });
            }
        });

        this.slackApp.error((error) => {
            slackLogger.error({ msg: 'Error on SlackApp', error });
        });
    }

    /**
     * @deprecated
     */
    registerForEventsLegacy() {
        slackLogger.debug('Register for events');
        this.rtm.on('authenticated', () => {
            slackLogger.info('Connected to Slack');
        });

        this.rtm.on('unable_to_rtm_start', () => {
            this.slackBridge.disconnect();
        });

        this.rtm.on('disconnected', () => {
            slackLogger.info('Disconnected from Slack');
            this.slackBridge.disconnect();
        });

        /**
         * Event fired when someone messages a channel the bot is in
         * {
         *    type: 'message',
         *     channel: [channel_id],
         *     user: [user_id],
         *     text: [message],
         *     ts: [ts.milli],
         *     team: [team_id],
         *     subtype: [message_subtype],
         *     inviter: [message_subtype = 'group_join|channel_join' -> user_id]
         * }
         **/
        this.rtm.on('message', async (slackMessage) => {
            slackLogger.debug('OnSlackEvent-MESSAGE: ', slackMessage);
            if (slackMessage) {
                try {
                    await this.onMessage(slackMessage);
                } catch (err) {
                    slackLogger.error({ msg: 'Unhandled error onMessage', err });
                }
            }
        });

        this.rtm.on('reaction_added', async (reactionMsg) => {
            slackLogger.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg);
            if (reactionMsg) {
                try {
                    await this.onReactionAdded(reactionMsg);
                } catch (err) {
                    slackLogger.error({ msg: 'Unhandled error onReactionAdded', err });
                }
            }
        });

        this.rtm.on('reaction_removed', async (reactionMsg) => {
            slackLogger.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg);
            if (reactionMsg) {
                try {
                    await this.onReactionRemoved(reactionMsg);
                } catch (err) {
                    slackLogger.error({ msg: 'Unhandled error onReactionRemoved', err });
                }
            }
        });

        /**
         * Event fired when someone creates a public channel
         * {
         *    type: 'channel_created',
         *    channel: {
         *        id: [channel_id],
         *        is_channel: true,
         *        name: [channel_name],
         *        created: [ts],
         *        creator: [user_id],
         *        is_shared: false,
         *        is_org_shared: false
         *    },
         *    event_ts: [ts.milli]
         * }
         **/
        this.rtm.on('channel_created', () => {});

        /**
         * Event fired when the bot joins a public channel
         * {
         *     type: 'channel_joined',
         *     channel: {
         *         id: [channel_id],
         *         name: [channel_name],
         *         is_channel: true,
         *         created: [ts],
         *         creator: [user_id],
         *         is_archived: false,
         *         is_general: false,
         *         is_member: true,
         *         last_read: [ts.milli],
         *         latest: [message_obj],
         *         unread_count: 0,
         *         unread_count_display: 0,
         *         members: [ user_ids ],
         *         topic: {
         *             value: [channel_topic],
         *             creator: [user_id],
         *             last_set: 0
         *         },
         *         purpose: {
         *             value: [channel_purpose],
         *             creator: [user_id],
         *             last_set: 0
         *         }
         *     }
         * }
         **/
        this.rtm.on('channel_joined', () => {});

        /**
         * Event fired when the bot leaves (or is removed from) a public channel
         * {
         *     type: 'channel_left',
         *     channel: [channel_id]
         * }
         **/
        this.rtm.on('channel_left', (channelLeftMsg) => {
            slackLogger.debug('OnSlackEvent-CHANNEL_LEFT: ', channelLeftMsg);
            if (channelLeftMsg) {
                try {
                    this.onChannelLeft(channelLeftMsg);
                } catch (err) {
                    slackLogger.error({ msg: 'Unhandled error onChannelLeft', err });
                }
            }
        });

        /**
         * Event fired when an archived channel is deleted by an admin
         * {
         *     type: 'channel_deleted',
         *     channel: [channel_id],
         *    event_ts: [ts.milli]
         * }
         **/
        this.rtm.on('channel_deleted', () => {});

        /**
         * Event fired when the channel has its name changed
         * {
         *     type: 'channel_rename',
         *     channel: {
         *         id: [channel_id],
         *         name: [channel_name],
         *         is_channel: true,
         *         created: [ts]
         *     },
         *    event_ts: [ts.milli]
         * }
         **/
        this.rtm.on('channel_rename', () => {});

        /**
         * Event fired when the bot joins a private channel
         * {
         *     type: 'group_joined',
         *     channel: {
         *         id: [channel_id],
         *         name: [channel_name],
         *         is_group: true,
         *         created: [ts],
         *         creator: [user_id],
         *         is_archived: false,
         *         is_mpim: false,
         *         is_open: true,
         *         last_read: [ts.milli],
         *         latest: [message_obj],
         *         unread_count: 0,
         *         unread_count_display: 0,
         *         members: [ user_ids ],
         *         topic: {
         *             value: [channel_topic],
         *             creator: [user_id],
         *             last_set: 0
         *         },
         *         purpose: {
         *             value: [channel_purpose],
         *             creator: [user_id],
         *             last_set: 0
         *         }
         *     }
         * }
         **/
        this.rtm.on('group_joined', () => {});

        /**
         * Event fired when the bot leaves (or is removed from) a private channel
         * {
         *     type: 'group_left',
         *     channel: [channel_id]
         * }
         **/
        this.rtm.on('group_left', () => {});

        /**
         * Event fired when the private channel has its name changed
         * {
         *     type: 'group_rename',
         *     channel: {
         *         id: [channel_id],
         *         name: [channel_name],
         *         is_group: true,
         *         created: [ts]
         *     },
         *    event_ts: [ts.milli]
         * }
         **/
        this.rtm.on('group_rename', () => {});

        /**
         * Event fired when a new user joins the team
         * {
         *     type: 'team_join',
         *     user:
         *     {
         *         id: [user_id],
         *         team_id: [team_id],
         *         name: [user_name],
         *         deleted: false,
         *         status: null,
         *         color: [color_code],
         *         real_name: '',
         *         tz: [timezone],
         *         tz_label: [timezone_label],
         *         tz_offset: [timezone_offset],
         *         profile:
         *         {
         *             avatar_hash: '',
         *             real_name: '',
         *             real_name_normalized: '',
         *             email: '',
         *             image_24: '',
         *             image_32: '',
         *             image_48: '',
         *             image_72: '',
         *             image_192: '',
         *             image_512: '',
         *             fields: null
         *         },
         *         is_admin: false,
         *         is_owner: false,
         *         is_primary_owner: false,
         *         is_restricted: false,
         *         is_ultra_restricted: false,
         *         is_bot: false,
         *         presence: [user_presence]
         *     },
         *     cache_ts: [ts]
         * }
         **/
        this.rtm.on('team_join', () => {});
    }

    /*
     https://api.slack.com/events/reaction_removed
     */
    async onReactionRemoved(slackReactionMsg) {
        if (slackReactionMsg) {
            if (!this.slackBridge.isReactionsEnabled) {
                return;
            }
            const rocketUser = await this.rocket.getUser(slackReactionMsg.user);
            // Lets find our Rocket originated message
            let rocketMsg = await Messages.findOneBySlackTs(slackReactionMsg.item.ts);

            if (!rocketMsg) {
                // Must have originated from Slack
                const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts);
                rocketMsg = await Messages.findOneById(rocketID);
            }

            if (rocketMsg && rocketUser) {
                const rocketReaction = `:${slackReactionMsg.reaction}:`;
                const theReaction = (rocketMsg.reactions || {})[rocketReaction];

                // If the Rocket user has already been removed, then this is an echo back from slack
                if (rocketMsg.reactions && theReaction) {
                    if (rocketUser.roles.includes('bot')) {
                        return;
                    }
                    if (theReaction.usernames.indexOf(rocketUser.username) === -1) {
                        return; // Reaction already removed
                    }
                } else {
                    // Reaction already removed
                    return;
                }

                // Stash this away to key off it later so we don't send it back to Slack
                this.slackBridge.reactionsMap.set(`unset${rocketMsg._id}${rocketReaction}`, rocketUser);
                slackLogger.debug('Removing reaction from Slack');
                await executeSetReaction(rocketUser._id, rocketReaction, rocketMsg._id);
            }
        }
    }

    /*
     https://api.slack.com/events/reaction_added
     */
    async onReactionAdded(slackReactionMsg) {
        if (slackReactionMsg) {
            if (!this.slackBridge.isReactionsEnabled) {
                return;
            }
            const rocketUser = await this.rocket.getUser(slackReactionMsg.user);

            if (rocketUser.roles.includes('bot')) {
                return;
            }

            // Lets find our Rocket originated message
            let rocketMsg = await Messages.findOneBySlackTs(slackReactionMsg.item.ts);

            if (!rocketMsg) {
                // Must have originated from Slack
                const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts);
                rocketMsg = await Messages.findOneById(rocketID);
            }

            if (rocketMsg && rocketUser) {
                const rocketReaction = `:${slackReactionMsg.reaction}:`;

                // If the Rocket user has already reacted, then this is Slack echoing back to us
                if (rocketMsg.reactions) {
                    const theReaction = rocketMsg.reactions[rocketReaction];
                    if (theReaction) {
                        if (theReaction.usernames.indexOf(rocketUser.username) !== -1) {
                            return; // Already reacted
                        }
                    }
                }

                // Stash this away to key off it later so we don't send it back to Slack
                this.slackBridge.reactionsMap.set(`set${rocketMsg._id}${rocketReaction}`, rocketUser);
                slackLogger.debug('Adding reaction from Slack');
                await executeSetReaction(rocketUser._id, rocketReaction, rocketMsg._id);
            }
        }
    }

    onChannelLeft(channelLeftMsg) {
        this.removeSlackChannel(channelLeftMsg.channel);
    }

    /**
     * We have received a message from slack and we need to save/delete/update it into rocket
     * https://api.slack.com/events/message
     */
    async onMessage(slackMessage, isImporting) {
        const isAFileShare = slackMessage && slackMessage.files && Array.isArray(slackMessage.files) && slackMessage.files.length;
        if (isAFileShare) {
            await this.processFileShare(slackMessage);
            return;
        }
        if (slackMessage.subtype) {
            switch (slackMessage.subtype) {
                case 'message_deleted':
                    await this.processMessageDeleted(slackMessage);
                    break;
                case 'message_changed':
                    await this.processMessageChanged(slackMessage);
                    break;
                case 'channel_join':
                    await this.processChannelJoin(slackMessage);
                    break;
                default:
                    // Keeping backwards compatability for now, refactor later
                    await this.processNewMessage(slackMessage, isImporting);
            }
        } else {
            // Simple message
            await this.processNewMessage(slackMessage, isImporting);
        }
    }

    async postFindChannel(rocketChannelName) {
        slackLogger.debug('Searching for Slack channel or group', rocketChannelName);
        const channels = await this.slackAPI.getChannels();
        if (channels && channels.length > 0) {
            for (const channel of channels) {
                if (channel.name === rocketChannelName && channel.is_member === true) {
                    return channel;
                }
            }
        }
        const groups = await this.slackAPI.getGroups();
        if (groups && groups.length > 0) {
            for (const group of groups) {
                if (group.name === rocketChannelName) {
                    return group;
                }
            }
        }
    }

    /**
     * Retrieves the Slack TS from a Rocket msg that originated from Slack
     * @param rocketMsg
     * @returns Slack TS or undefined if not a message that originated from slack
     * @private
     */
    getTimeStamp(rocketMsg) {
        // slack-G3KJGGE15-1483081061-000169
        let slackTS;
        let index = rocketMsg._id.indexOf('slack-');
        if (index === 0) {
            // This is a msg that originated from Slack
            slackTS = rocketMsg._id.substr(6, rocketMsg._id.length);
            index = slackTS.indexOf('-');
            slackTS = slackTS.substr(index + 1, slackTS.length);
            slackTS = slackTS.replace('-', '.');
        } else {
            // This probably originated as a Rocket msg, but has been sent to Slack
            slackTS = rocketMsg.slackTs;
        }

        return slackTS;
    }

    /**
     * Adds a slack channel to our collection that the rocketbot is a member of on slack
     * @param rocketChID
     * @param slackChID
     */
    addSlackChannel(rocketChID, slackChID) {
        const ch = this.getSlackChannel(rocketChID);
        if (ch == null) {
            slackLogger.debug('Added channel', { rocketChID, slackChID });
            this.slackChannelRocketBotMembershipMap.set(rocketChID, {
                id: slackChID,
                family: slackChID.charAt(0) === 'C' ? 'channels' : 'groups',
            });
        }
    }

    removeSlackChannel(slackChID) {
        const keys = this.slackChannelRocketBotMembershipMap.keys();
        let slackChannel;
        let key;
        while ((key = keys.next().value) != null) {
            slackChannel = this.slackChannelRocketBotMembershipMap.get(key);
            if (slackChannel.id === slackChID) {
                // Found it, need to delete it
                this.slackChannelRocketBotMembershipMap.delete(key);
                break;
            }
        }
    }

    getSlackChannel(rocketChID) {
        return this.slackChannelRocketBotMembershipMap.get(rocketChID);
    }

    async populateMembershipChannelMapByChannels() {
        const channels = await this.slackAPI.getChannels();
        if (!channels || channels.length <= 0) {
            return;
        }

        for await (const slackChannel of channels) {
            const rocketchat_room =
                (await Rooms.findOneByName(slackChannel.name, { projection: { _id: 1 } })) ||
                (await Rooms.findOneByImportId(slackChannel.id, { projection: { _id: 1 } }));
            if (rocketchat_room && slackChannel.is_member) {
                this.addSlackChannel(rocketchat_room._id, slackChannel.id);
            }
        }
    }

    async populateMembershipChannelMapByGroups() {
        const groups = await this.slackAPI.getGroups();
        if (!groups || groups.length <= 0) {
            return;
        }

        for await (const slackGroup of groups) {
            const rocketchat_room =
                (await Rooms.findOneByName(slackGroup.name, { projection: { _id: 1 } })) ||
                (await Rooms.findOneByImportId(slackGroup.id, { projection: { _id: 1 } }));
            if (rocketchat_room && slackGroup.is_member) {
                this.addSlackChannel(rocketchat_room._id, slackGroup.id);
            }
        }
    }

    async populateMembershipChannelMap() {
        slackLogger.debug('Populating channel map');
        await this.populateMembershipChannelMapByChannels();
        await this.populateMembershipChannelMapByGroups();
    }

    /*
     https://api.slack.com/methods/reactions.add
     */
    async postReactionAdded(reaction, slackChannel, slackTS) {
        if (reaction && slackChannel && slackTS) {
            const data = {
                name: reaction,
                channel: slackChannel,
                timestamp: slackTS,
            };

            slackLogger.debug('Posting Add Reaction to Slack');
            const postResult = await this.slackAPI.react(data);
            if (postResult) {
                slackLogger.debug('Reaction added to Slack');
            }
        }
    }

    /*
     https://api.slack.com/methods/reactions.remove
     */
    async postReactionRemove(reaction, slackChannel, slackTS) {
        if (reaction && slackChannel && slackTS) {
            const data = {
                name: reaction,
                channel: slackChannel,
                timestamp: slackTS,
            };

            slackLogger.debug('Posting Remove Reaction to Slack');
            const postResult = await this.slackAPI.removeReaction(data);
            if (postResult) {
                slackLogger.debug('Reaction removed from Slack');
            }
        }
    }

    async postDeleteMessage(rocketMessage) {
        if (rocketMessage) {
            const slackChannel = this.getSlackChannel(rocketMessage.rid);

            if (slackChannel != null) {
                const data = {
                    ts: this.getTimeStamp(rocketMessage),
                    channel: this.getSlackChannel(rocketMessage.rid).id,
                    as_user: true,
                };

                slackLogger.debug('Post Delete Message to Slack', data);
                const postResult = await this.slackAPI.removeMessage(data);
                if (postResult) {
                    slackLogger.debug('Message deleted on Slack');
                }
            }
        }
    }

    storeMessageBeingSent(data) {
        this.messagesBeingSent.push(data);
    }

    removeMessageBeingSent(data) {
        const idx = this.messagesBeingSent.indexOf(data);
        if (idx >= 0) {
            this.messagesBeingSent.splice(idx, 1);
        }
    }

    isMessageBeingSent(username, channel) {
        if (!this.messagesBeingSent.length) {
            return false;
        }

        return this.messagesBeingSent.some((messageData) => {
            if (messageData.username !== username) {
                return false;
            }

            if (messageData.channel !== channel) {
                return false;
            }

            return true;
        });
    }

    createSlackMessageId(ts, channelId) {
        return `slack${channelId ? `-${channelId}` : ''}-${ts.replace(/\./g, '-')}`;
    }

    async postMessage(slackChannel, rocketMessage) {
        if (slackChannel && slackChannel.id) {
            let iconUrl = getUserAvatarURL(rocketMessage.u && rocketMessage.u.username);
            if (iconUrl) {
                iconUrl = Meteor.absoluteUrl().replace(/\/$/, '') + iconUrl;
            }
            const data = {
                text: rocketMessage.msg,
                channel: slackChannel.id,
                username: rocketMessage.u && rocketMessage.u.username,
                icon_url: iconUrl,
                link_names: 1,
            };

            if (rocketMessage.tmid) {
                const tmessage = await Messages.findOneById(rocketMessage.tmid);
                if (tmessage && tmessage.slackTs) {
                    data.thread_ts = tmessage.slackTs;
                }
            }
            slackLogger.debug('Post Message To Slack', data);

            // If we don't have the bot id yet and we have multiple slack bridges, we need to keep track of the messages that are being sent
            if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) {
                this.storeMessageBeingSent(data);
            }

            const postResult = await this.slackAPI.sendMessage(data);

            if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) {
                this.removeMessageBeingSent(data);
            }

            if (postResult && postResult.message && postResult.message.bot_id && postResult.message.ts) {
                this.slackBotId = postResult.message.bot_id;
                await Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.message.bot_id, postResult.message.ts);
                slackLogger.debug(`RocketMsgID=${rocketMessage._id} SlackMsgID=${postResult.message.ts} SlackBotID=${postResult.message.bot_id}`);
            }
        }
    }

    /*
     https://api.slack.com/methods/chat.update
     */
    async postMessageUpdate(slackChannel, rocketMessage) {
        if (slackChannel && slackChannel.id) {
            const data = {
                ts: this.getTimeStamp(rocketMessage),
                channel: slackChannel.id,
                text: rocketMessage.msg,
                as_user: true,
            };
            slackLogger.debug('Post UpdateMessage To Slack', data);
            const postResult = await this.slackAPI.updateMessage(data);
            if (postResult) {
                slackLogger.debug('Message updated on Slack');
            }
        }
    }

    async processMemberJoinChannel(event, context) {
        slackLogger.debug('Member join channel', event.channel);
        const rocketCh = await this.rocket.getChannel({ channel: event.channel });
        if (rocketCh != null) {
            this.addSlackChannel(rocketCh._id, event.channel);
            if (context?.botUserId !== event?.user) {
                const rocketChatUser = await this.rocket.getUser(event.user);
                await addUserToRoom(rocketCh._id, rocketChatUser);
            }
        }
    }

    async processChannelJoin(slackMessage) {
        slackLogger.debug('Channel join', slackMessage.channel.id);
        const rocketCh = await this.rocket.addChannel(slackMessage.channel);
        if (rocketCh != null) {
            this.addSlackChannel(rocketCh._id, slackMessage.channel);
        }
    }

    async processFileShare(slackMessage) {
        if (!settings.get('SlackBridge_FileUpload_Enabled')) {
            return;
        }
        const file = slackMessage.files[0];

        if (file && file.url_private_download !== undefined) {
            const rocketChannel = await this.rocket.getChannel(slackMessage);
            const rocketUser = await this.rocket.getUser(slackMessage.user);

            // Hack to notify that a file was attempted to be uploaded
            delete slackMessage.subtype;

            // If the text includes the file link, simply use the same text for the rocket message.
            // If the link was not included, then use it instead of the message.

            if (slackMessage.text.indexOf(file.permalink) < 0) {
                slackMessage.text = file.permalink;
            }

            const ts = new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000);
            const msgDataDefaults = {
                _id: this.rocket.createRocketID(slackMessage.channel, slackMessage.ts),
                ts,
                updatedBySlack: true,
            };

            await this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, false);
        }
    }

    /*
     https://api.slack.com/events/message/message_deleted
     */
    async processMessageDeleted(slackMessage) {
        if (slackMessage.previous_message) {
            const rocketChannel = await this.rocket.getChannel(slackMessage);
            const rocketUser = await Users.findOneById('rocket.cat', { projection: { username: 1 } });

            if (rocketChannel && rocketUser) {
                // Find the Rocket message to delete
                let rocketMsgObj = await Messages.findOneBySlackBotIdAndSlackTs(
                    slackMessage.previous_message.bot_id,
                    slackMessage.previous_message.ts,
                );

                if (!rocketMsgObj) {
                    // Must have been a Slack originated msg
                    const _id = this.rocket.createRocketID(slackMessage.channel, slackMessage.previous_message.ts);
                    rocketMsgObj = await Messages.findOneById(_id);
                }

                if (rocketMsgObj) {
                    await deleteMessage(rocketMsgObj, rocketUser);
                    slackLogger.debug('Rocket message deleted by Slack');
                }
            }
        }
    }

    /*
     https://api.slack.com/events/message/message_changed
     */
    async processMessageChanged(slackMessage) {
        if (slackMessage.previous_message) {
            const currentMsg = await Messages.findOneById(this.rocket.createRocketID(slackMessage.channel, slackMessage.message.ts));

            // Only process this change, if its an actual update (not just Slack repeating back our Rocket original change)
            if (currentMsg && slackMessage.message.text !== currentMsg.msg) {
                const rocketChannel = await this.rocket.getChannel(slackMessage);
                const rocketUser = slackMessage.previous_message.user
                    ? (await this.rocket.findUser(slackMessage.previous_message.user)) ||
                      (await this.rocket.addUser(slackMessage.previous_message.user))
                    : null;

                const rocketMsgObj = {
                    // @TODO _id
                    _id: this.rocket.createRocketID(slackMessage.channel, slackMessage.previous_message.ts),
                    rid: rocketChannel._id,
                    msg: await this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.message.text),
                    updatedBySlack: true, // We don't want to notify slack about this change since Slack initiated it
                };

                await updateMessage(rocketMsgObj, rocketUser);
                slackLogger.debug('Rocket message updated by Slack');
            }
        }
    }

    /*
     This method will get refactored and broken down into single responsibilities
     */
    async processNewMessage(slackMessage, isImporting) {
        const rocketChannel = await this.rocket.getChannel(slackMessage);
        let rocketUser = null;
        if (slackMessage.subtype === 'bot_message') {
            rocketUser = await Users.findOneById('rocket.cat', { projection: { username: 1 } });
        } else {
            rocketUser = slackMessage.user
                ? (await this.rocket.findUser(slackMessage.user)) || (await this.rocket.addUser(slackMessage.user))
                : null;
        }
        if (rocketChannel && rocketUser) {
            const msgDataDefaults = {
                _id: this.rocket.createRocketID(slackMessage.channel, slackMessage.ts),
                ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
            };
            if (isImporting) {
                msgDataDefaults.imported = 'slackbridge';
            }
            try {
                await this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting, this);
            } catch (e) {
                // http://www.mongodb.org/about/contributors/error-codes/
                // 11000 == duplicate key error
                if (e.name === 'MongoError' && e.code === 11000) {
                    return;
                }

                throw e;
            }
        }
    }

    async processBotMessage(rocketChannel, slackMessage) {
        const excludeBotNames = settings.get('SlackBridge_ExcludeBotnames');
        if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) {
            return;
        }

        if (this.slackBotId) {
            if (slackMessage.bot_id === this.slackBotId) {
                return;
            }
        } else {
            const slackChannel = this.getSlackChannel(rocketChannel._id);
            if (this.isMessageBeingSent(slackMessage.username || slackMessage.bot_id, slackChannel.id)) {
                return;
            }
        }

        const rocketMsgObj = {
            msg: await this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text),
            rid: rocketChannel._id,
            bot: true,
            attachments: slackMessage.attachments,
            username: slackMessage.username || slackMessage.bot_id,
        };
        this.rocket.addAliasToMsg(slackMessage.username || slackMessage.bot_id, rocketMsgObj);
        if (slackMessage.icons) {
            rocketMsgObj.emoji = slackMessage.icons.emoji;
        }
        return rocketMsgObj;
    }

    async processMeMessage(rocketUser, slackMessage) {
        return this.rocket.addAliasToMsg(rocketUser.username, {
            msg: `_${await this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text)}_`,
        });
    }

    async processChannelJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (isImporting) {
            await Message.saveSystemMessage('uj', rocketChannel._id, rocketUser.username, rocketUser, {
                ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                imported: 'slackbridge',
            });
        } else {
            await addUserToRoom(rocketChannel._id, rocketUser);
        }
    }

    async processGroupJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (slackMessage.inviter) {
            const inviter = slackMessage.inviter
                ? (await this.rocket.findUser(slackMessage.inviter)) || (await this.rocket.addUser(slackMessage.inviter))
                : null;
            if (isImporting) {
                await Message.saveSystemMessage('au', rocketChannel._id, rocketUser.username, inviter, {
                    ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                    imported: 'slackbridge',
                });
            } else {
                await addUserToRoom(rocketChannel._id, rocketUser, inviter);
            }
        }
    }

    async processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (isImporting) {
            await Message.saveSystemMessage('ul', rocketChannel._id, rocketUser.username, rocketUser, {
                ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                imported: 'slackbridge',
            });
        } else {
            await removeUserFromRoom(rocketChannel._id, rocketUser);
        }
    }

    async processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (isImporting) {
            await Message.saveSystemMessage('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, {
                ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                imported: 'slackbridge',
            });
        } else {
            await saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false);
        }
    }

    async processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (isImporting) {
            await Message.saveSystemMessage('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, {
                ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                imported: 'slackbridge',
            });
        } else {
            await saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false);
        }
    }

    async processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (isImporting) {
            await Message.saveSystemMessage('r', rocketChannel._id, slackMessage.name, rocketUser, {
                ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                imported: 'slackbridge',
            });
        } else {
            await saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false);
        }
    }

    async processShareMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (slackMessage.file && slackMessage.file.url_private_download !== undefined) {
            const details = {
                message_id: this.createSlackMessageId(slackMessage.ts),
                name: slackMessage.file.name,
                size: slackMessage.file.size,
                type: slackMessage.file.mimetype,
                rid: rocketChannel._id,
            };
            return this.uploadFileFromSlack(
                details,
                slackMessage.file.url_private_download,
                rocketUser,
                rocketChannel,
                new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
                isImporting,
            );
        }
    }

    async processPinnedItemMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        if (slackMessage.attachments && slackMessage.attachments[0] && slackMessage.attachments[0].text) {
            // TODO: refactor this logic to use the service to send this system message instead of using sendMessage
            const rocketMsgObj = {
                rid: rocketChannel._id,
                t: 'message_pinned',
                msg: '',
                u: {
                    _id: rocketUser._id,
                    username: rocketUser.username,
                },
                attachments: [
                    {
                        text: await this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text),
                        author_name: slackMessage.attachments[0].author_subname,
                        author_icon: getUserAvatarURL(slackMessage.attachments[0].author_subname),
                        ts: new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000),
                    },
                ],
            };

            if (!isImporting && slackMessage.attachments[0].channel_id && slackMessage.attachments[0].ts) {
                const messageId = this.createSlackMessageId(slackMessage.attachments[0].ts, slackMessage.attachments[0].channel_id);
                await Messages.setPinnedByIdAndUserId(messageId, rocketMsgObj.u, true, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000));
                if (settings.get('Message_Read_Receipt_Store_Users')) {
                    await ReadReceipts.setPinnedByMessageId(messageId, true);
                }
            }

            return rocketMsgObj;
        }
        slackLogger.error('Pinned item with no attachment');
    }

    async processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
        switch (slackMessage.subtype) {
            case 'bot_message':
                return this.processBotMessage(rocketChannel, slackMessage);
            case 'me_message':
                return this.processMeMessage(rocketUser, slackMessage);
            case 'channel_join':
                return this.processChannelJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'group_join':
                return this.processGroupJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'channel_leave':
            case 'group_leave':
                return this.processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'channel_topic':
            case 'group_topic':
                return this.processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'channel_purpose':
            case 'group_purpose':
                return this.processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'channel_name':
            case 'group_name':
                return this.processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'channel_archive':
            case 'group_archive':
                if (!isImporting) {
                    await archiveRoom(rocketChannel, rocketUser);
                }
                return;
            case 'channel_unarchive':
            case 'group_unarchive':
                if (!isImporting) {
                    await unarchiveRoom(rocketChannel);
                }
                return;
            case 'file_share':
                return this.processShareMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'file_comment':
                slackLogger.error('File comment not implemented');
                return;
            case 'file_mention':
                slackLogger.error('File mentioned not implemented');
                return;
            case 'pinned_item':
                return this.processPinnedItemMessage(rocketChannel, rocketUser, slackMessage, isImporting);
            case 'unpinned_item':
                slackLogger.error('Unpinned item not implemented');
        }
    }

    /**
    Uploads the file to the storage.
    @param [Object] details an object with details about the upload. name, size, type, and rid
    @param [String] fileUrl url of the file to download/import
    @param [Object] user the Rocket.Chat user
    @param [Object] room the Rocket.Chat room
    @param [Date] timeStamp the timestamp the file was uploaded
    **/
    // details, slackMessage.file.url_private_download, rocketUser, rocketChannel, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), isImporting);
    async uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) {
        const requestModule = /https/i.test(slackFileURL) ? https : http;
        const parsedUrl = url.parse(slackFileURL, true);
        parsedUrl.headers = { Authorization: `Bearer ${this.apiToken}` };
        await requestModule.get(parsedUrl, async (stream) => {
            const fileStore = FileUpload.getStore('Uploads');

            const file = await fileStore.insert(details, stream);

            const url = file.url.replace(Meteor.absoluteUrl(), '/');
            const attachment = {
                title: file.name,
                title_link: url,
            };

            if (/^image\/.+/.test(file.type)) {
                attachment.image_url = url;
                attachment.image_type = file.type;
                attachment.image_size = file.size;
                attachment.image_dimensions = file.identify && file.identify.size;
            }
            if (/^audio\/.+/.test(file.type)) {
                attachment.audio_url = url;
                attachment.audio_type = file.type;
                attachment.audio_size = file.size;
            }
            if (/^video\/.+/.test(file.type)) {
                attachment.video_url = url;
                attachment.video_type = file.type;
                attachment.video_size = file.size;
            }

            const msg = {
                rid: details.rid,
                ts: timeStamp,
                msg: '',
                file: {
                    _id: file._id,
                },
                groupable: false,
                attachments: [attachment],
            };

            if (isImporting) {
                msg.imported = 'slackbridge';
            }

            if (details.message_id && typeof details.message_id === 'string') {
                msg._id = details.message_id;
            }

            void sendMessage(rocketUser, msg, rocketChannel, true);
        });
    }

    async importFromHistory(options) {
        slackLogger.debug('Importing messages history');
        const data = await this.slackAPI.getHistory(options);
        if (Array.isArray(data.messages) && data.messages.length) {
            let latest = 0;
            for await (const message of data.messages.reverse()) {
                slackLogger.debug('MESSAGE: ', message);
                if (!latest || message.ts > latest) {
                    latest = message.ts;
                }
                message.channel = options.channel;
                await this.onMessage(message, true);
            }
            return { has_more: data.has_more, ts: latest };
        }
    }

    async copyChannelInfo(rid, channelMap) {
        slackLogger.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid);
        const channel = await this.slackAPI.getRoomInfo(channelMap.id);
        if (channel) {
            const members = await this.slackAPI.getMembers(channelMap.id);
            if (members && Array.isArray(members) && members.length) {
                for await (const member of members) {
                    const user = (await this.rocket.findUser(member)) || (await this.rocket.addUser(member));
                    if (user) {
                        slackLogger.debug('Adding user to room', user.username, rid);
                        await addUserToRoom(rid, user, null, { skipSystemMessage: true });
                    }
                }
            }

            let topic = '';
            let topic_last_set = 0;
            let topic_creator = null;
            if (channel && channel.topic && channel.topic.value) {
                topic = channel.topic.value;
                topic_last_set = channel.topic.last_set;
                topic_creator = channel.topic.creator;
            }

            if (channel && channel.purpose && channel.purpose.value) {
                if (topic_last_set) {
                    if (topic_last_set < channel.purpose.last_set) {
                        topic = channel.purpose.topic;
                        topic_creator = channel.purpose.creator;
                    }
                } else {
                    topic = channel.purpose.topic;
                    topic_creator = channel.purpose.creator;
                }
            }

            if (topic) {
                const creator = (await this.rocket.findUser(topic_creator)) || (await this.rocket.addUser(topic_creator));
                slackLogger.debug('Setting room topic', rid, topic, creator.username);
                await saveRoomTopic(rid, topic, creator, false);
            }
        }
    }

    async copyPins(rid, channelMap) {
        const items = await this.slackAPI.getPins(channelMap.id);
        if (items && Array.isArray(items) && items.length) {
            for await (const pin of items) {
                if (pin.message) {
                    const user = await this.rocket.findUser(pin.message.user);
                    // TODO: send this system message to the room as well (using the service)
                    const msgObj = {
                        rid,
                        t: 'message_pinned',
                        msg: '',
                        u: {
                            _id: user._id,
                            username: user.username,
                        },
                        attachments: [
                            {
                                text: await this.rocket.convertSlackMsgTxtToRocketTxtFormat(pin.message.text),
                                author_name: user.username,
                                author_icon: getUserAvatarURL(user.username),
                                ts: new Date(parseInt(pin.message.ts.split('.')[0]) * 1000),
                            },
                        ],
                    };

                    const messageId = this.createSlackMessageId(pin.message.ts, pin.channel);
                    await Messages.setPinnedByIdAndUserId(messageId, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000));
                    if (settings.get('Message_Read_Receipt_Store_Users')) {
                        await ReadReceipts.setPinnedByMessageId(messageId, true);
                    }
                }
            }
        }
    }

    async importMessages(rid, callback) {
        slackLogger.info('importMessages: ', rid);
        const rocketchat_room = await Rooms.findOneById(rid);
        if (rocketchat_room) {
            if (this.getSlackChannel(rid)) {
                await this.copyChannelInfo(rid, this.getSlackChannel(rid));

                slackLogger.debug('Importing messages from Slack to Rocket.Chat', this.getSlackChannel(rid), rid);

                let results = await this.importFromHistory({
                    channel: this.getSlackChannel(rid).id,
                    oldest: 1,
                });
                while (results && results.has_more) {
                    // eslint-disable-next-line no-await-in-loop
                    results = await this.importFromHistory({
                        channel: this.getSlackChannel(rid).id,
                        oldest: results.ts,
                    });
                }

                slackLogger.debug('Pinning Slack channel messages to Rocket.Chat', this.getSlackChannel(rid), rid);
                await this.copyPins(rid, this.getSlackChannel(rid));

                return callback();
            }
            const slack_room = await this.postFindChannel(rocketchat_room.name);
            if (slack_room) {
                this.addSlackChannel(rid, slack_room.id);
                return this.importMessages(rid, callback);
            }
            slackLogger.error({ msg: 'Could not find Slack room with specified name', roomName: rocketchat_room.name });
            return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name'));
        }
        slackLogger.error({ msg: 'Could not find Rocket.Chat room with specified id', rid });
        return callback(new Meteor.Error('error-invalid-room', 'Invalid room'));
    }
}