import url from 'url'; import http from 'http'; import https from 'https'; import { RTMClient } from '@slack/rtm-api'; import { Meteor } from 'meteor/meteor'; import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Message } from '@rocket.chat/core-services'; import { slackLogger } from './logger'; import { SlackAPI } from './SlackAPI'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; import { settings } from '../../settings/server'; import { deleteMessage, updateMessage, addUserToRoom, removeUserFromRoom, unarchiveRoom, sendMessage } from '../../lib/server'; import { archiveRoom } from '../../lib/server/functions/archiveRoom'; import { saveRoomName, saveRoomTopic } from '../../channel-settings/server'; import { FileUpload } from '../../file-upload/server'; import { executeSetReaction } from '../../reactions/server/setReaction'; 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 // 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 = {}; } /** * Connect to the remote Slack server using the passed in token API and register for Slack events * @param apiToken */ async connect(apiToken) { this.apiToken = apiToken; if (RTMClient != null) { RTMClient.disconnect; } this.slackAPI = new SlackAPI(this.apiToken); this.rtm = new RTMClient(this.apiToken); await this.rtm .start() .then((res) => slackLogger.debug('Connecting to slack', res)) .catch((err) => { slackLogger.error({ msg: 'Error attempting to connect to Slack', err }); if (err.data.error === 'invalid_auth') { throw new Error('The provided token is invalid'); } throw new Error(err); }); this.registerForEvents(); 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 }); this.slackBridge.disconnect(); } }); } /** * Unregister for slack events and disconnect from Slack */ disconnect() { if (this.rtm.connected && this.rtm.disconnect) { this.rtm.disconnect(); } } setRocket(rocket) { this.rocket = rocket; } registerForEvents() { 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}:`; // If the Rocket user has already been removed, then this is an echo back from slack if (rocketMsg.reactions) { const theReaction = rocketMsg.reactions[rocketReaction]; if (theReaction) { 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 = { token: this.apiToken, 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 = { token: this.apiToken, 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 = { token: this.apiToken, 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; }); } 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 = { token: this.apiToken, 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 = { token: this.apiToken, 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 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: `slack-${slackMessage.ts.replace(/\./g, '-')}`, 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) { 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) { await Messages.setPinnedByIdAndUserId( `slack-${slackMessage.attachments[0].channel_id}-${slackMessage.attachments[0].ts.replace(/\./g, '-')}`, rocketMsgObj.u, true, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), ); } 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(family, options) { slackLogger.debug('Importing messages history'); const data = await this.slackAPI.getHistory(family, 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, 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); 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), }, ], }; await Messages.setPinnedByIdAndUserId( `slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000), ); } } } } 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(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: 1, }); while (results && results.has_more) { // eslint-disable-next-line no-await-in-loop results = await this.importFromHistory(this.getSlackChannel(rid).family, { 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')); } }