From 66ab6525d0b85525ab20c68f411ca347a7c99a2d Mon Sep 17 00:00:00 2001 From: kablewi Date: Tue, 7 Mar 2017 16:26:30 -0800 Subject: [PATCH 01/11] Support for dynamic slack and rocket channels --- packages/rocketchat-slackbridge/README.md | 2 +- packages/rocketchat-slackbridge/logger.js | 4 +- packages/rocketchat-slackbridge/package.js | 5 + packages/rocketchat-slackbridge/rocket.js | 421 +++++ packages/rocketchat-slackbridge/slack.js | 1084 +++++++++++++ .../rocketchat-slackbridge/slackbridge.js | 1381 +---------------- .../tests/manual-tests.txt | 58 + 7 files changed, 1628 insertions(+), 1327 deletions(-) create mode 100644 packages/rocketchat-slackbridge/rocket.js create mode 100644 packages/rocketchat-slackbridge/slack.js create mode 100644 packages/rocketchat-slackbridge/tests/manual-tests.txt diff --git a/packages/rocketchat-slackbridge/README.md b/packages/rocketchat-slackbridge/README.md index 5ce00d638c50..babd07e43f09 100644 --- a/packages/rocketchat-slackbridge/README.md +++ b/packages/rocketchat-slackbridge/README.md @@ -27,7 +27,7 @@ The following can be configured in your Rocket.Chat Administration SlackBridge p #### Group Chat Messages * Send and receive basic messages -* Delete messages +* Delete messages (Can't delete slack message from rocket) * Edit messages (Slack doesn't allow editing of BOT messages, so can't edit a Rocket msg in Slack) * React to messages (as BOT in Slack) diff --git a/packages/rocketchat-slackbridge/logger.js b/packages/rocketchat-slackbridge/logger.js index a64654f619d2..eac5c2ae6ffc 100644 --- a/packages/rocketchat-slackbridge/logger.js +++ b/packages/rocketchat-slackbridge/logger.js @@ -5,6 +5,8 @@ logger = new Logger('SlackBridge', { sections: { connection: 'Connection', events: 'Events', - class: 'Class' + class: 'Class', + slack: 'Slack', + rocket: 'Rocket' } }); diff --git a/packages/rocketchat-slackbridge/package.js b/packages/rocketchat-slackbridge/package.js index fc18d4da1b77..27d8a6c6ef52 100644 --- a/packages/rocketchat-slackbridge/package.js +++ b/packages/rocketchat-slackbridge/package.js @@ -15,8 +15,13 @@ Package.onUse(function(api) { api.addFiles('logger.js', 'server'); api.addFiles('settings.js', 'server'); + api.addFiles('rocket.js', 'server'); + api.addFiles('slack.js', 'server'); api.addFiles('slackbridge.js', 'server'); api.addFiles('slashcommand/slackbridge_import.server.js', 'server'); + + api.export("SB_SlackAdapter", 'server'); + api.export("SB_RocketAdapter", 'server'); }); Npm.depends({ diff --git a/packages/rocketchat-slackbridge/rocket.js b/packages/rocketchat-slackbridge/rocket.js new file mode 100644 index 000000000000..8c472790d27c --- /dev/null +++ b/packages/rocketchat-slackbridge/rocket.js @@ -0,0 +1,421 @@ +/* globals logger SB_RocketAdapter */ + +class RocketAdapter { + constructor(slackBridge) { + logger.rocket.debug('constructor'); + this.slackBridge = slackBridge; + this.util = Npm.require('util'); + + this.userTags = {}; + this.slack = {}; + } + + connect() { + this.registerForEvents(); + } + + disconnect() { + this.unregisterForEvents(); + } + + setSlack(slack) { + this.slack = slack; + } + + registerForEvents() { + logger.rocket.debug('Register for events'); + RocketChat.callbacks.add('afterSaveMessage', this.onMessage.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Out'); + RocketChat.callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Delete'); + RocketChat.callbacks.add('setReaction', this.onSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_SetReaction'); + RocketChat.callbacks.add('unsetReaction', this.onUnSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); + } + + unregisterForEvents() { + logger.rocket.debug('Unregister for events'); + RocketChat.callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); + RocketChat.callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); + RocketChat.callbacks.remove('setReaction', 'SlackBridge_SetReaction'); + RocketChat.callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction'); + } + + onMessageDelete(rocketMessageDeleted) { + try { + if (! this.slack.getSlackChannel(rocketMessageDeleted.rid)) { + //This is on a channel that the rocket bot is not subscribed + return; + } + logger.rocket.debug('onRocketMessageDelete', rocketMessageDeleted); + + this.slack.postDeleteMessage(rocketMessageDeleted); + } catch (err) { + logger.rocket.error('Unhandled error onMessageDelete', err); + } + } + + onSetReaction(rocketMsgID, reaction) { + try { + logger.rocket.debug('onRocketSetReaction'); + + if (rocketMsgID && reaction) { + if (this.slackBridge.reactionsMap.delete('set' + rocketMsgID + reaction)) { + //This was a Slack reaction, we don't need to tell Slack about it + return; + } + const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); + if (rocketMsg) { + const slackChannel = this.slack.getSlackChannel(rocketMsg.rid); + if (null != slackChannel) { + const slackTS = this.slack.getTimeStamp(rocketMsg); + this.slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS); + } + } + } + } catch (err) { + logger.rocket.error('Unhandled error onSetReaction', err); + } + } + + onUnSetReaction(rocketMsgID, reaction) { + try { + logger.rocket.debug('onRocketUnSetReaction'); + + if (rocketMsgID && reaction) { + if (this.slackBridge.reactionsMap.delete('unset' + rocketMsgID + reaction)) { + //This was a Slack unset reaction, we don't need to tell Slack about it + return; + } + + const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); + if (rocketMsg) { + const slackChannel = this.slack.getSlackChannel(rocketMsg.rid); + if (null != slackChannel) { + const slackTS = this.slack.getTimeStamp(rocketMsg); + this.slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS); + } + } + } + } catch (err) { + logger.rocket.error('Unhandled error onUnSetReaction', err); + } + } + + onMessage(rocketMessage) { + try { + if (! this.slack.getSlackChannel(rocketMessage.rid)) { + //This is on a channel that the rocket bot is not subscribed + return; + } + logger.rocket.debug('onRocketMessage', rocketMessage); + + if (rocketMessage.editedAt) { + //This is an Edit Event + this.processMessageChanged(rocketMessage); + return rocketMessage; + } + // Ignore messages originating from Slack + if (rocketMessage._id.indexOf('slack-') === 0) { + return rocketMessage; + } + + //A new message from Rocket.Chat + this.processSendMessage(rocketMessage); + } catch (err) { + logger.rocket.error('Unhandled error onMessage', err); + } + + return rocketMessage; + } + + processSendMessage(rocketMessage) { + //Since we got this message, SlackBridge_Out_Enabled is true + + if (RocketChat.settings.get('SlackBridge_Out_All') === true) { + this.slack.postMessage(this.slack.getSlackChannel(rocketMessage.rid), rocketMessage); + } else { + //They want to limit to certain groups + const outSlackChannels = _.pluck(RocketChat.settings.get('SlackBridge_Out_Channels'), '_id') || []; + //logger.rocket.debug('Out SlackChannels: ', outSlackChannels); + if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) { + this.slack.postMessage(this.slack.getSlackChannel(rocketMessage.rid), rocketMessage); + } + } + } + + processMessageChanged(rocketMessage) { + if (rocketMessage) { + if (rocketMessage.updatedBySlack) { + //We have already processed this + delete rocketMessage.updatedBySlack; + return; + } + + //This was a change from Rocket.Chat + const slackChannel = this.slack.getSlackChannel(rocketMessage.rid); + this.slack.postMessageUpdate(slackChannel, rocketMessage); + } + } + + + getChannel(slackMessage) { + return slackMessage.channel ? this.findChannel(slackMessage.channel) || this.addChannel(slackMessage.channel) : null; + } + + getUser(slackUser) { + return slackUser ? this.findUser(slackUser) || this.addUser(slackUser) : null; + } + + createRocketID(slackChannel, ts) { + return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`; + } + + findChannel(slackChannelId) { + return RocketChat.models.Rooms.findOneByImportId(slackChannelId); + } + + addChannel(slackChannelID, hasRetried = false) { + logger.rocket.debug('Adding Rocket.Chat channel from Slack', slackChannelID); + let slackResults = null; + let isGroup = false; + if (slackChannelID.charAt(0) === 'C') { + slackResults = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.slackBridge.apiToken, channel: slackChannelID } }); + } else if (slackChannelID.charAt(0) === 'G') { + slackResults = HTTP.get('https://slack.com/api/groups.info', { params: { token: this.slackBridge.apiToken, channel: slackChannelID } }); + isGroup = true; + } + if (slackResults && slackResults.data && slackResults.data.ok === true) { + const rocketChannelData = isGroup ? slackResults.data.group : slackResults.data.channel; + const existingRocketRoom = RocketChat.models.Rooms.findOneByName(rocketChannelData.name); + + // If the room exists, make sure we have its id in importIds + if (existingRocketRoom || rocketChannelData.is_general) { + rocketChannelData.rocketId = rocketChannelData.is_general ? 'GENERAL' : existingRocketRoom._id; + RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); + } else { + const rocketUsers = []; + for (const member of rocketChannelData.members) { + if (member !== rocketChannelData.creator) { + const rocketUser = this.findUser(member) || this.addUser(member); + if (rocketUser && rocketUser.username) { + rocketUsers.push(rocketUser.username); + } + } + } + const rocketUserCreator = rocketChannelData.creator ? this.findUser(rocketChannelData.creator) || this.addUser(rocketChannelData.creator) : null; + if (!rocketUserCreator) { + logger.rocket.error('Could not fetch room creator information', rocketChannelData.creator); + return; + } + + try { + const rocketChannel = RocketChat.createRoom(isGroup ? 'p' : 'c', rocketChannelData.name, rocketUserCreator.username, rocketUsers); + rocketChannelData.rocketId = rocketChannel.rid; + } catch (e) { + if (!hasRetried) { + logger.rocket.debug('Error adding channel from Slack. Will retry in 1s.', e.message); + // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. + Meteor._sleepForMs(1000); + return this.findChannel(slackChannelID) || this.addChannel(slackChannelID, true); + } else { + console.log(e.message); + } + } + + const roomUpdate = { + ts: new Date(rocketChannelData.created * 1000) + }; + let lastSetTopic = 0; + if (!_.isEmpty(rocketChannelData.topic && rocketChannelData.topic.value)) { + roomUpdate.topic = rocketChannelData.topic.value; + lastSetTopic = rocketChannelData.topic.last_set; + } + if (!_.isEmpty(rocketChannelData.purpose && rocketChannelData.purpose.value) && rocketChannelData.purpose.last_set > lastSetTopic) { + roomUpdate.topic = rocketChannelData.purpose.value; + } + RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); + this.slack.addSlackChannel(rocketChannelData.rocketId, slackChannelID); + } + return RocketChat.models.Rooms.findOneById(rocketChannelData.rocketId); + } + logger.rocket.debug('Channel not added'); + return; + } + + findUser(slackUserID) { + const rocketUser = RocketChat.models.Users.findOneByImportId(slackUserID); + if (rocketUser && !this.userTags[slackUserID]) { + this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUser.username}` }; + } + return rocketUser; + } + + addUser(slackUserID) { + logger.rocket.debug('Adding Rocket.Chat user from Slack', slackUserID); + const slackResults = HTTP.get('https://slack.com/api/users.info', { params: { token: this.slackBridge.apiToken, user: slackUserID } }); + if (slackResults && slackResults.data && slackResults.data.ok === true && slackResults.data.user) { + const rocketUserData = slackResults.data.user; + const isBot = rocketUserData.is_bot === true; + const email = rocketUserData.profile && rocketUserData.profile.email || ''; + let existingRocketUser; + if (!isBot) { + existingRocketUser = RocketChat.models.Users.findOneByEmailAddress(email) || RocketChat.models.Users.findOneByUsername(rocketUserData.name); + } else { + existingRocketUser = RocketChat.models.Users.findOneByUsername(rocketUserData.name); + } + + if (existingRocketUser) { + rocketUserData.rocketId = existingRocketUser._id; + rocketUserData.name = existingRocketUser.username; + } else { + const newUser = { + password: Random.id(), + username: rocketUserData.name + }; + + if (!isBot && email) { + newUser.email = email; + } + + if (isBot) { + newUser.joinDefaultChannels = false; + } + + rocketUserData.rocketId = Accounts.createUser(newUser); + const userUpdate = { + utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, + roles: isBot ? [ 'bot' ] : [ 'user' ] + }; + + if (rocketUserData.profile && rocketUserData.profile.real_name) { + userUpdate['name'] = rocketUserData.profile.real_name; + } + + if (rocketUserData.deleted) { + userUpdate['active'] = false; + userUpdate['services.resume.loginTokens'] = []; + } + + RocketChat.models.Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate }); + + const user = RocketChat.models.Users.findOneById(rocketUserData.rocketId); + + let url = null; + if (rocketUserData.profile) { + if (rocketUserData.profile.image_original) { + url = rocketUserData.profile.image_original; + } else if (rocketUserData.profile.image_512) { + url = rocketUserData.profile.image_512; + } + } + if (url) { + try { + RocketChat.setUserAvatar(user, url, null, 'url'); + } catch (error) { + logger.rocket.debug('Error setting user avatar', error.message); + } + } + } + + const importIds = [ rocketUserData.id ]; + if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) { + importIds.push(rocketUserData.profile.bot_id); + } + RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); + if (!this.userTags[slackUserID]) { + this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUserData.name}` }; + } + return RocketChat.models.Users.findOneById(rocketUserData.rocketId); + } + logger.rocket.debug('User not added'); + return; + } + + addAliasToMsg(rocketUserName, rocketMsgObj) { + var aliasFormat = RocketChat.settings.get('SlackBridge_AliasFormat'); + if (aliasFormat) { + var alias = this.util.format(aliasFormat, rocketUserName); + + if (alias !== rocketUserName) { + rocketMsgObj.alias = alias; + } + } + + return rocketMsgObj; + } + + createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting) { + if (slackMessage.type === 'message') { + let rocketMsgObj = {}; + if (!_.isEmpty(slackMessage.subtype)) { + rocketMsgObj = this.slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting); + if (!rocketMsgObj) { + return; + } + } else { + rocketMsgObj = { + msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), + rid: rocketChannel._id, + u: { + _id: rocketUser._id, + username: rocketUser.username + } + }; + + this.addAliasToMsg(rocketUser.username, rocketMsgObj); + } + _.extend(rocketMsgObj, rocketMsgDataDefaults); + if (slackMessage.edited) { + rocketMsgObj.editedAt = new Date(parseInt(slackMessage.edited.ts.split('.')[0]) * 1000); + } + if (slackMessage.subtype === 'bot_message') { + rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); + } + + if (slackMessage.pinned_to && slackMessage.pinned_to.indexOf(slackMessage.channel) !== -1) { + rocketMsgObj.pinned = true; + rocketMsgObj.pinnedAt = Date.now; + rocketMsgObj.pinnedBy = _.pick(rocketUser, '_id', 'username'); + } + if (slackMessage.subtype === 'bot_message') { + Meteor.setTimeout(() => { + if (slackMessage.bot_id && slackMessage.ts && !RocketChat.models.Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) { + RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); + } + }, 500); + } else { + logger.rocket.debug('Send message to Rocket.Chat'); + RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); + } + } + } + + convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) { + if (!_.isEmpty(slackMsgTxt)) { + slackMsgTxt = slackMsgTxt.replace(//g, '@all'); + slackMsgTxt = slackMsgTxt.replace(//g, '@all'); + slackMsgTxt = slackMsgTxt.replace(/>/g, '<'); + slackMsgTxt = slackMsgTxt.replace(/</g, '>'); + slackMsgTxt = slackMsgTxt.replace(/&/g, '&'); + slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:'); + slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:'); + slackMsgTxt = slackMsgTxt.replace(/:piggy:/g, ':pig:'); + slackMsgTxt = slackMsgTxt.replace(/:uk:/g, ':gb:'); + slackMsgTxt = slackMsgTxt.replace(/<(http[s]?:[^>]*)>/g, '$1'); + + slackMsgTxt.replace(/(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g, (match, userId) => { + if (!this.userTags[userId]) { + this.findUser(userId) || this.addUser(userId); // This adds userTags for the userId + } + const userTags = this.userTags[userId]; + if (userTags) { + slackMsgTxt = slackMsgTxt.replace(userTags.slack, userTags.rocket); + } + }); + } else { + slackMsgTxt = ''; + } + return slackMsgTxt; + } + +} + +SB_RocketAdapter = RocketAdapter; diff --git a/packages/rocketchat-slackbridge/slack.js b/packages/rocketchat-slackbridge/slack.js new file mode 100644 index 000000000000..6b27164c810d --- /dev/null +++ b/packages/rocketchat-slackbridge/slack.js @@ -0,0 +1,1084 @@ +/* globals logger SB_SlackAdapter */ + +class SlackAdapter { + + constructor(slackBridge) { + logger.slack.debug('constructor'); + this.slackBridge = slackBridge; + this.slackClient = Npm.require('slack-client'); + 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 = {}; + } + + /** + * Connect to the remote Slack server using the passed in token API and register for Slack events + * @param apiToken + */ + connect(apiToken) { + this.apiToken = apiToken; + + var RtmClient = this.slackClient.RtmClient; + if (null != RtmClient) { + RtmClient.disconnect; + } + this.rtm = new RtmClient(this.apiToken); + this.rtm.start(); + this.registerForEvents(); + + Meteor.startup(() => { + try { + this.populateMembershipChannelMap(); // If run outside of Meteor.startup, HTTP is not defined + } catch (err) { + logger.slack.error('Error attempting to connect to Slack', err); + this.slackBridge.disconnect(); + } + }); + } + + /** + * Unregister for slack events and disconnect from Slack + */ + disconnect() { + this.rtm.disconnect && this.rtm.disconnect; + } + + setRocket(rocket) { + this.rocket = rocket; + } + + registerForEvents() { + logger.slack.debug('Register for events'); + var CLIENT_EVENTS = this.slackClient.CLIENT_EVENTS; + this.rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, () => { + logger.slack.info('Connected to Slack'); + }); + + this.rtm.on(CLIENT_EVENTS.RTM.UNABLE_TO_RTM_START, () => { + this.slackBridge.disconnect(); + }); + + this.rtm.on(CLIENT_EVENTS.RTM.DISCONNECT, () => { + logger.slack.info('Disconnected from Slack'); + this.slackBridge.disconnect(); + }); + + var RTM_EVENTS = this.slackClient.RTM_EVENTS; + + /** + * 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(RTM_EVENTS.MESSAGE, Meteor.bindEnvironment((slackMessage) => { + logger.slack.debug('OnSlackEvent-MESSAGE: ', slackMessage); + if (slackMessage) { + try { + this.onMessage(slackMessage); + } catch (err) { + logger.slack.error('Unhandled error onMessage', err); + } + } + })); + + this.rtm.on(RTM_EVENTS.REACTION_ADDED, Meteor.bindEnvironment((reactionMsg) => { + logger.slack.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); + if (reactionMsg) { + try { + this.onReactionAdded(reactionMsg); + } catch (err) { + logger.slack.error('Unhandled error onReactionAdded', err); + } + } + })); + + this.rtm.on(RTM_EVENTS.REACTION_REMOVED, Meteor.bindEnvironment((reactionMsg) => { + logger.slack.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); + if (reactionMsg) { + try { + this.onReactionRemoved(reactionMsg); + } catch (err) { + logger.slack.error('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(RTM_EVENTS.CHANNEL_CREATED, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.CHANNEL_JOINED, Meteor.bindEnvironment(() => {})); + + /** + * Event fired when the bot leaves (or is removed from) a public channel + * { + * type: 'channel_left', + * channel: [channel_id] + * } + **/ + this.rtm.on(RTM_EVENTS.CHANNEL_LEFT, Meteor.bindEnvironment((channelLeftMsg) => { + logger.slack.debug('OnSlackEvent-CHANNEL_LEFT: ', channelLeftMsg); + if (channelLeftMsg) { + try { + this.onChannelLeft(channelLeftMsg); + } catch (err) { + logger.slack.error('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(RTM_EVENTS.CHANNEL_DELETED, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.CHANNEL_RENAME, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.GROUP_JOINED, Meteor.bindEnvironment(() => {})); + + /** + * Event fired when the bot leaves (or is removed from) a private channel + * { + * type: 'group_left', + * channel: [channel_id] + * } + **/ + this.rtm.on(RTM_EVENTS.GROUP_LEFT, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.GROUP_RENAME, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.TEAM_JOIN, Meteor.bindEnvironment(() => {})); + } + + /* + https://api.slack.com/events/reaction_removed + */ + onReactionRemoved(slackReactionMsg) { + if (slackReactionMsg) { + const rocketUser = this.rocket.getUser(slackReactionMsg.user); + //Lets find our Rocket originated message + let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); + + if (!rocketMsg) { + //Must have originated from Slack + const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); + rocketMsg = RocketChat.models.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); + logger.slack.debug('Removing reaction from Slack'); + Meteor.runAsUser(rocketUser._id, () => { + Meteor.call('setReaction', rocketReaction, rocketMsg._id); + }); + } + } + } + + /* + https://api.slack.com/events/reaction_added + */ + onReactionAdded(slackReactionMsg) { + if (slackReactionMsg) { + const rocketUser = this.rocket.getUser(slackReactionMsg.user); + + if (rocketUser.roles.includes('bot')) { + return; + } + + //Lets find our Rocket originated message + let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); + + if (!rocketMsg) { + //Must have originated from Slack + const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); + rocketMsg = RocketChat.models.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); + logger.slack.debug('Adding reaction from Slack'); + Meteor.runAsUser(rocketUser._id, () => { + Meteor.call('setReaction', 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 + */ + onMessage(slackMessage, isImporting) { + if (slackMessage.subtype) { + switch (slackMessage.subtype) { + case 'message_deleted': + this.processMessageDeleted(slackMessage); + break; + case 'message_changed': + this.processMessageChanged(slackMessage); + break; + case 'channel_join': + this.processChannelJoin(slackMessage); + break; + default: + //Keeping backwards compatability for now, refactor later + this.processNewMessage(slackMessage, isImporting); + } + } else { + //Simple message + this.processNewMessage(slackMessage, isImporting); + } + } + + postGetChannelInfo(slackChID) { + logger.slack.debug('Getting slack channel info', slackChID); + const response = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.apiToken, channel: slackChID } }); + if (response && response.data) { + return response.data.channel; + } + } + + postFindChannel(rocketChannelName) { + logger.slack.debug('Searching for Slack channel or group', rocketChannelName); + let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { + for (const channel of response.data.channels) { + if (channel.name === rocketChannelName && channel.is_member === true) { + return channel; + } + } + } + response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { + for (const group of response.data.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 (null == ch) { + 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); + } + + populateMembershipChannelMapByChannels() { + const response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { + for (const slackChannel of response.data.channels) { + const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } }); + if (rocketchat_room) { + if (slackChannel.is_member) { + this.addSlackChannel(rocketchat_room._id, slackChannel.id); + } + } + } + } + } + + populateMembershipChannelMapByGroups() { + const response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { + for (const slackGroup of response.data.groups) { + const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } }); + if (rocketchat_room) { + if (slackGroup.is_member) { + this.addSlackChannel(rocketchat_room._id, slackGroup.id); + } + } + } + } + } + + populateMembershipChannelMap() { + logger.slack.debug('Populating channel map'); + this.populateMembershipChannelMapByChannels(); + this.populateMembershipChannelMapByGroups(); + } + + /* + https://api.slack.com/methods/reactions.add + */ + postReactionAdded(reaction, slackChannel, slackTS) { + if (reaction && slackChannel && slackTS) { + const data = { + token: this.apiToken, + name: reaction, + channel: slackChannel, + timestamp: slackTS + }; + + logger.slack.debug('Posting Add Reaction to Slack'); + const postResult = HTTP.post('https://slack.com/api/reactions.add', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Reaction added to Slack'); + } + } + } + + /* + https://api.slack.com/methods/reactions.remove + */ + postReactionRemove(reaction, slackChannel, slackTS) { + if (reaction && slackChannel && slackTS) { + const data = { + token: this.apiToken, + name: reaction, + channel: slackChannel, + timestamp: slackTS + }; + + logger.slack.debug('Posting Remove Reaction to Slack'); + const postResult = HTTP.post('https://slack.com/api/reactions.remove', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Reaction removed from Slack'); + } + } + } + + postDeleteMessage(rocketMessage) { + if (rocketMessage) { + var slackChannel = this.getSlackChannel(rocketMessage.rid); + + if (null != slackChannel) { + const data = { + token: this.apiToken, + ts: this.getTimeStamp(rocketMessage), + channel: this.getSlackChannel(rocketMessage.rid).id, + as_user: true + }; + + logger.slack.debug('Post Delete Message to Slack', data); + const postResult = HTTP.post('https://slack.com/api/chat.delete', {params: data}); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Message deleted on Slack'); + } + } + } + } + + postMessage(slackChannel, rocketMessage) { + if (slackChannel && slackChannel.id) { + let iconUrl = getAvatarUrlFromUsername(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 + }; + logger.slack.debug('Post Message To Slack', data); + const postResult = HTTP.post('https://slack.com/api/chat.postMessage', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { + RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); + logger.slack.debug('RocketMsgID=' + rocketMessage._id + ' SlackMsgID=' + postResult.data.message.ts + ' SlackBotID=' + postResult.data.message.bot_id); + } + } + } + + /* + https://api.slack.com/methods/chat.update + */ + 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 + }; + logger.slack.debug('Post UpdateMessage To Slack', data); + const postResult = HTTP.post('https://slack.com/api/chat.update', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Message updated on Slack'); + } + } + } + + processChannelJoin(slackMessage) { + logger.slack.debug('Channel join', slackMessage.channel.id); + const rocketCh = this.rocket.addChannel(slackMessage.channel); + if (null != rocketCh) { + this.addSlackChannel(rocketCh._id, slackMessage.channel); + } + } + + /* + https://api.slack.com/events/message/message_deleted + */ + processMessageDeleted(slackMessage) { + if (slackMessage.previous_message) { + const rocketChannel = this.rocket.getChannel(slackMessage); + const rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); + + if (rocketChannel && rocketUser) { + //Find the Rocket message to delete + let rocketMsgObj = RocketChat.models.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 = RocketChat.models.Messages.findOneById(_id); + } + + if (rocketMsgObj) { + RocketChat.deleteMessage(rocketMsgObj, rocketUser); + logger.slack.debug('Rocket message deleted by Slack'); + } + } + } + } + + /* + https://api.slack.com/events/message/message_changed + */ + processMessageChanged(slackMessage) { + if (slackMessage.previous_message) { + const currentMsg = RocketChat.models.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 = this.rocket.getChannel(slackMessage); + const rocketUser = slackMessage.previous_message.user ? this.rocket.findUser(slackMessage.previous_message.user) || 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: this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.message.text), + updatedBySlack: true //We don't want to notify slack about this change since Slack initiated it + }; + + RocketChat.updateMessage(rocketMsgObj, rocketUser); + logger.slack.debug('Rocket message updated by Slack'); + } + } + } + + /* + This method will get refactored and broken down into single responsibilities + */ + processNewMessage(slackMessage, isImporting) { + const rocketChannel = this.rocket.getChannel(slackMessage); + let rocketUser = null; + if (slackMessage.subtype === 'bot_message') { + rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); + } else { + rocketUser = slackMessage.user ? this.rocket.findUser(slackMessage.user) || 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 { + this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting); + } catch (e) { + // http://www.mongodb.org/about/contributors/error-codes/ + // 11000 == duplicate key error + if (e.name === 'MongoError' && e.code === 11000) { + return; + } + + throw e; + } + } + } + + + processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + let rocketMsgObj = null; + switch (slackMessage.subtype) { + case 'bot_message': + var excludeBotNames = RocketChat.settings.get('SlackBridge_Botnames'); + if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) { + return; + } + + rocketMsgObj = { + msg: 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; + case 'me_message': + return this.rocket.addAliasToMsg(rocketUser.username, { + msg: `_${this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text)}_` + }); + case 'channel_join': + if (isImporting) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.addUserToRoom(rocketChannel._id, rocketUser); + } + return; + case 'group_join': + if (slackMessage.inviter) { + const inviter = slackMessage.inviter ? this.rocket.findUser(slackMessage.inviter) || this.rocket.addUser(slackMessage.inviter) : null; + if (isImporting) { + RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, { + ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), + u: { + _id: inviter._id, + username: inviter.username + }, + imported: 'slackbridge' + }); + } else { + RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter); + } + } + return; + case 'channel_leave': + case 'group_leave': + if (isImporting) { + RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, { + ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), + imported: 'slackbridge' + }); + } else { + RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser); + } + return; + case 'channel_topic': + case 'group_topic': + if (isImporting) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false); + } + return; + case 'channel_purpose': + case 'group_purpose': + if (isImporting) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false); + } + return; + case 'channel_name': + case 'group_name': + if (isImporting) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false); + } + return; + case 'channel_archive': + case 'group_archive': + if (!isImporting) { + RocketChat.archiveRoom(rocketChannel); + } + return; + case 'channel_unarchive': + case 'group_unarchive': + if (!isImporting) { + RocketChat.unarchiveRoom(rocketChannel); + } + return; + case 'file_share': + 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); + } + break; + case 'file_comment': + logger.slack.error('File comment not implemented'); + return; + case 'file_mention': + logger.slack.error('File mentioned not implemented'); + return; + case 'pinned_item': + if (slackMessage.attachments && slackMessage.attachments[0] && slackMessage.attachments[0].text) { + rocketMsgObj = { + rid: rocketChannel._id, + t: 'message_pinned', + msg: '', + u: { + _id: rocketUser._id, + username: rocketUser.username + }, + attachments: [{ + 'text' : this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text), + 'author_name' : slackMessage.attachments[0].author_subname, + 'author_icon' : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname), + 'ts' : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000) + }] + }; + + if (!isImporting) { + RocketChat.models.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; + } else { + logger.slack.error('Pinned item with no attachment'); + } + return; + case 'unpinned_item': + logger.slack.error('Unpinned item not implemented'); + return; + } + } + + /** + 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); + uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) { + const url = Npm.require('url'); + const requestModule = /https/i.test(slackFileURL) ? Npm.require('https') : Npm.require('http'); + var parsedUrl = url.parse(slackFileURL, true); + parsedUrl.headers = { 'Authorization': 'Bearer ' + this.apiToken }; + requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => { + const fileId = Meteor.fileStore.create(details); + if (fileId) { + Meteor.fileStore.write(stream, fileId, (err, file) => { + console.log('fileStore.write', file); + if (err) { + throw new Error(err); + } else { + const url = file.url.replace(Meteor.absoluteUrl(), '/'); + const attachment = { + title: `File Uploaded: ${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; + } + + return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); + } + }); + } + })); + } + + importFromHistory(family, options) { + logger.slack.debug('Importing messages history'); + const response = HTTP.get('https://slack.com/api/' + family + '.history', { params: _.extend({ token: this.apiToken }, options) }); + if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) { + let latest = 0; + for (const message of response.data.messages.reverse()) { + logger.slack.debug('MESSAGE: ', message); + if (!latest || message.ts > latest) { + latest = message.ts; + } + message.channel = options.channel; + this.onMessage(message, true); + } + return { has_more: response.data.has_more, ts: latest }; + } + } + + copyChannelInfo(rid, channelMap) { + logger.slack.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); + const response = HTTP.get('https://slack.com/api/' + channelMap.family + '.info', { params: { token: this.apiToken, channel: channelMap.id } }); + if (response && response.data) { + const data = channelMap.family === 'channels' ? response.data.channel : response.data.group; + if (data && _.isArray(data.members) && data.members.length > 0) { + for (const member of data.members) { + const user = this.rocket.findUser(member) || this.rocket.addUser(member); + if (user) { + logger.slack.debug('Adding user to room', user.username, rid); + RocketChat.addUserToRoom(rid, user, null, true); + } + } + } + + let topic = ''; + let topic_last_set = 0; + let topic_creator = null; + if (data && data.topic && data.topic.value) { + topic = data.topic.value; + topic_last_set = data.topic.last_set; + topic_creator = data.topic.creator; + } + + if (data && data.purpose && data.purpose.value) { + if (topic_last_set) { + if (topic_last_set < data.purpose.last_set) { + topic = data.purpose.topic; + topic_creator = data.purpose.creator; + } + } else { + topic = data.purpose.topic; + topic_creator = data.purpose.creator; + } + } + + if (topic) { + const creator = this.rocket.findUser(topic_creator) || this.rocket.addUser(topic_creator); + logger.slack.debug('Setting room topic', rid, topic, creator.username); + RocketChat.saveRoomTopic(rid, topic, creator, false); + } + } + } + + copyPins(rid, channelMap) { + const response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } }); + if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) { + for (const pin of response.data.items) { + if (pin.message) { + const user = this.rocket.findUser(pin.message.user); + const msgObj = { + rid: rid, + t: 'message_pinned', + msg: '', + u: { + _id: user._id, + username: user.username + }, + attachments: [{ + 'text' : this.rocket.convertSlackMsgTxtToRocketTxtFormat(pin.message.text), + 'author_name' : user.username, + 'author_icon' : getAvatarUrlFromUsername(user.username), + 'ts' : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000) + }] + }; + + RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000)); + } + } + } + } + + importMessages(rid, callback) { + logger.slack.info('importMessages: ', rid); + const rocketchat_room = RocketChat.models.Rooms.findOneById(rid); + if (rocketchat_room) { + if (this.getSlackChannel(rid)) { + this.copyChannelInfo(rid, this.getSlackChannel(rid)); + + logger.slack.debug('Importing messages from Slack to Rocket.Chat', this.getSlackChannel(rid), rid); + let results = this.importFromHistory(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: 1 }); + while (results && results.has_more) { + results = this.importFromHistory(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: results.ts }); + } + + logger.slack.debug('Pinning Slack channel messages to Rocket.Chat', this.getSlackChannel(rid), rid); + this.copyPins(rid, this.getSlackChannel(rid)); + + return callback(); + } else { + const slack_room = this.postFindChannel(rocketchat_room.name); + if (slack_room) { + this.addSlackChannel(rid, slack_room.id); + return this.importMessages(rid, callback); + } else { + logger.slack.error('Could not find Slack room with specified name', rocketchat_room.name); + return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name')); + } + } + } else { + logger.slack.error('Could not find Rocket.Chat room with specified id', rid); + return callback(new Meteor.Error('error-invalid-room', 'Invalid room')); + } + } + +} + +SB_SlackAdapter = SlackAdapter; + diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index 4881fee53bd7..616e1760a33e 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -1,1365 +1,96 @@ -/* globals logger */ +/* globals logger SB_SlackAdapter SB_RocketAdapter*/ +/** + * SlackBridge interfaces between this Rocket installation and a remote Slack installation. + */ class SlackBridge { constructor() { - this.util = Npm.require('util'); - this.slackClient = Npm.require('slack-client'); - this.apiToken = RocketChat.settings.get('SlackBridge_APIToken'); - this.aliasFormat = RocketChat.settings.get('SlackBridge_AliasFormat'); - this.excludeBotnames = RocketChat.settings.get('SlackBridge_Botnames'); - this.rtm = {}; + this.slack = new SB_SlackAdapter(this); + this.rocket = new SB_RocketAdapter(this); + this.reactionsMap = new Map(); //Sync object between rocket and slack this.connected = false; - this.userTags = {}; - this.slackChannelMap = {}; - this.reactionsMap = new Map(); + this.rocket.setSlack(this.slack); + this.slack.setRocket(this.rocket); - RocketChat.settings.get('SlackBridge_APIToken', (key, value) => { - if (value !== this.apiToken) { - this.apiToken = value; - if (this.connected) { - this.disconnect(); - this.connect(); - } - } - }); - - RocketChat.settings.get('SlackBridge_AliasFormat', (key, value) => { - this.aliasFormat = value; - }); - - RocketChat.settings.get('SlackBridge_ExcludeBotnames', (key, value) => { - this.excludeBotnames = value; - }); - - RocketChat.settings.get('SlackBridge_Enabled', (key, value) => { - if (value && this.apiToken) { - this.connect(); - } else { - this.disconnect(); - } - }); + this.processSettings(); } connect() { if (this.connected === false) { + + this.slack.connect(this.apiToken); + if (RocketChat.settings.get('SlackBridge_Out_Enabled')) { + this.rocket.connect(); + } + this.connected = true; - logger.connection.info('Connecting via token: ', this.apiToken); - var RtmClient = this.slackClient.RtmClient; - this.rtm = new RtmClient(this.apiToken); - this.rtm.start(); - this.registerForSlackEvents(); - RocketChat.settings.get('SlackBridge_Out_Enabled', (key, value) => { - if (value) { - this.registerForRocketEvents(); - } else { - this.unregisterForRocketEvents(); - } - }); - Meteor.startup(() => { - try { - this.populateSlackChannelMap(); // If run outside of Meteor.startup, HTTP is not defined - } catch (err) { - logger.class.error('Error attempting to connect to Slack', err); - this.disconnect(); - } - }); + logger.connection.info('Enabled'); } } disconnect() { if (this.connected === true) { + this.rocket.disconnect(); + this.slack.disconnect(); this.connected = false; - this.rtm.disconnect && this.rtm.disconnect(); - logger.connection.info('Disconnected'); - this.unregisterForRocketEvents(); - } - } - - convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) { - if (!_.isEmpty(slackMsgTxt)) { - slackMsgTxt = slackMsgTxt.replace(//g, '@all'); - slackMsgTxt = slackMsgTxt.replace(//g, '@all'); - slackMsgTxt = slackMsgTxt.replace(/>/g, '<'); - slackMsgTxt = slackMsgTxt.replace(/</g, '>'); - slackMsgTxt = slackMsgTxt.replace(/&/g, '&'); - slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:'); - slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:'); - slackMsgTxt = slackMsgTxt.replace(/:piggy:/g, ':pig:'); - slackMsgTxt = slackMsgTxt.replace(/:uk:/g, ':gb:'); - slackMsgTxt = slackMsgTxt.replace(/<(http[s]?:[^>]*)>/g, '$1'); - - slackMsgTxt.replace(/(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g, (match, userId) => { - if (!this.userTags[userId]) { - this.findRocketUser(userId) || this.addRocketUser(userId); // This adds userTags for the userId - } - const userTags = this.userTags[userId]; - if (userTags) { - slackMsgTxt = slackMsgTxt.replace(userTags.slack, userTags.rocket); - } - }); - } else { - slackMsgTxt = ''; - } - return slackMsgTxt; - } - - findRocketChannel(slackChannelId) { - return RocketChat.models.Rooms.findOneByImportId(slackChannelId); - } - - addRocketChannel(slackChannelID, hasRetried = false) { - logger.class.debug('Adding Rocket.Chat channel from Slack', slackChannelID); - let slackResults = null; - let isGroup = false; - if (slackChannelID.charAt(0) === 'C') { - slackResults = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.apiToken, channel: slackChannelID } }); - } else if (slackChannelID.charAt(0) === 'G') { - slackResults = HTTP.get('https://slack.com/api/groups.info', { params: { token: this.apiToken, channel: slackChannelID } }); - isGroup = true; - } - if (slackResults && slackResults.data && slackResults.data.ok === true) { - const rocketChannelData = isGroup ? slackResults.data.group : slackResults.data.channel; - const existingRocketRoom = RocketChat.models.Rooms.findOneByName(rocketChannelData.name); - - // If the room exists, make sure we have its id in importIds - if (existingRocketRoom || rocketChannelData.is_general) { - rocketChannelData.rocketId = rocketChannelData.is_general ? 'GENERAL' : existingRocketRoom._id; - RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); - } else { - const rocketUsers = []; - for (const member of rocketChannelData.members) { - if (member !== rocketChannelData.creator) { - const rocketUser = this.findRocketUser(member) || this.addRocketUser(member); - if (rocketUser && rocketUser.username) { - rocketUsers.push(rocketUser.username); - } - } - } - const rocketUserCreator = rocketChannelData.creator ? this.findRocketUser(rocketChannelData.creator) || this.addRocketUser(rocketChannelData.creator) : null; - if (!rocketUserCreator) { - logger.class.error('Could not fetch room creator information', rocketChannelData.creator); - return; - } - - try { - const rocketChannel = RocketChat.createRoom(isGroup ? 'p' : 'c', rocketChannelData.name, rocketUserCreator.username, rocketUsers); - rocketChannelData.rocketId = rocketChannel.rid; - } catch (e) { - if (!hasRetried) { - logger.class.debug('Error adding channel from Slack. Will retry in 1s.', e.message); - // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. - Meteor._sleepForMs(1000); - return this.findRocketChannel(slackChannelID) || this.addRocketChannel(slackChannelID, true); - } else { - console.log(e.message); - } - } - - const roomUpdate = { - ts: new Date(rocketChannelData.created * 1000) - }; - let lastSetTopic = 0; - if (!_.isEmpty(rocketChannelData.topic && rocketChannelData.topic.value)) { - roomUpdate.topic = rocketChannelData.topic.value; - lastSetTopic = rocketChannelData.topic.last_set; - } - if (!_.isEmpty(rocketChannelData.purpose && rocketChannelData.purpose.value) && rocketChannelData.purpose.last_set > lastSetTopic) { - roomUpdate.topic = rocketChannelData.purpose.value; - } - RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); - this.slackChannelMap[rocketChannelData.rocketId] = { id: slackChannelID, family: slackChannelID.charAt(0) === 'C' ? 'channels' : 'groups' }; - } - return RocketChat.models.Rooms.findOneById(rocketChannelData.rocketId); - } - logger.class.debug('Channel not added'); - return; - } - - findRocketUser(slackUserID) { - const rocketUser = RocketChat.models.Users.findOneByImportId(slackUserID); - if (rocketUser && !this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUser.username}` }; - } - return rocketUser; - } - - addRocketUser(slackUserID) { - logger.class.debug('Adding Rocket.Chat user from Slack', slackUserID); - const slackResults = HTTP.get('https://slack.com/api/users.info', { params: { token: this.apiToken, user: slackUserID } }); - if (slackResults && slackResults.data && slackResults.data.ok === true && slackResults.data.user) { - const rocketUserData = slackResults.data.user; - const isBot = rocketUserData.is_bot === true; - const email = rocketUserData.profile && rocketUserData.profile.email || ''; - let existingRocketUser; - if (!isBot) { - existingRocketUser = RocketChat.models.Users.findOneByEmailAddress(email) || RocketChat.models.Users.findOneByUsername(rocketUserData.name); - } else { - existingRocketUser = RocketChat.models.Users.findOneByUsername(rocketUserData.name); - } - - if (existingRocketUser) { - rocketUserData.rocketId = existingRocketUser._id; - rocketUserData.name = existingRocketUser.username; - } else { - const newUser = { - password: Random.id(), - username: rocketUserData.name - }; - - if (!isBot && email) { - newUser.email = email; - } - - if (isBot) { - newUser.joinDefaultChannels = false; - } - - rocketUserData.rocketId = Accounts.createUser(newUser); - const userUpdate = { - utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, - roles: isBot ? [ 'bot' ] : [ 'user' ] - }; - - if (rocketUserData.profile && rocketUserData.profile.real_name) { - userUpdate['name'] = rocketUserData.profile.real_name; - } - - if (rocketUserData.deleted) { - userUpdate['active'] = false; - userUpdate['services.resume.loginTokens'] = []; - } - - RocketChat.models.Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate }); - - const user = RocketChat.models.Users.findOneById(rocketUserData.rocketId); - - let url = null; - if (rocketUserData.profile) { - if (rocketUserData.profile.image_original) { - url = rocketUserData.profile.image_original; - } else if (rocketUserData.profile.image_512) { - url = rocketUserData.profile.image_512; - } - } - if (url) { - try { - RocketChat.setUserAvatar(user, url, null, 'url'); - } catch (error) { - logger.class.debug('Error setting user avatar', error.message); - } - } - } - - const importIds = [ rocketUserData.id ]; - if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) { - importIds.push(rocketUserData.profile.bot_id); - } - RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); - if (!this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUserData.name}` }; - } - return RocketChat.models.Users.findOneById(rocketUserData.rocketId); - } - logger.class.debug('User not added'); - return; - } - - addAliasToRocketMsg(rocketUserName, rocketMsgObj) { - if (this.aliasFormat) { - var alias = this.util.format(this.aliasFormat, rocketUserName); - - if (alias !== rocketUserName) { - rocketMsgObj.alias = alias; - } - } - - return rocketMsgObj; - } - - createAndSaveRocketMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting) { - if (slackMessage.type === 'message') { - let rocketMsgObj = {}; - if (!_.isEmpty(slackMessage.subtype)) { - rocketMsgObj = this.processSlackSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting); - if (!rocketMsgObj) { - return; - } - } else { - rocketMsgObj = { - msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), - rid: rocketChannel._id, - u: { - _id: rocketUser._id, - username: rocketUser.username - } - }; - - this.addAliasToRocketMsg(rocketUser.username, rocketMsgObj); - } - _.extend(rocketMsgObj, rocketMsgDataDefaults); - if (slackMessage.edited) { - rocketMsgObj.editedAt = new Date(parseInt(slackMessage.edited.ts.split('.')[0]) * 1000); - } - if (slackMessage.subtype === 'bot_message') { - rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); - } - - if (slackMessage.pinned_to && slackMessage.pinned_to.indexOf(slackMessage.channel) !== -1) { - rocketMsgObj.pinned = true; - rocketMsgObj.pinnedAt = Date.now; - rocketMsgObj.pinnedBy = _.pick(rocketUser, '_id', 'username'); - } - if (slackMessage.subtype === 'bot_message') { - Meteor.setTimeout(() => { - if (slackMessage.bot_id && slackMessage.ts && !RocketChat.models.Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) { - RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); - } - }, 500); - } else { - logger.class.debug('Send message to Rocket.Chat'); - RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); - } - } - } - - /* - https://api.slack.com/events/reaction_removed - */ - onSlackReactionRemoved(slackReactionMsg) { - if (slackReactionMsg) { - const rocketUser = this.getRocketUser(slackReactionMsg.user); - //Lets find our Rocket originated message - let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); - - if (!rocketMsg) { - //Must have originated from Slack - const rocketID = this.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); - rocketMsg = RocketChat.models.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.reactionsMap.set('unset'+rocketMsg._id+rocketReaction, rocketUser); - logger.class.debug('Removing reaction from Slack'); - Meteor.runAsUser(rocketUser._id, () => { - Meteor.call('setReaction', rocketReaction, rocketMsg._id); - }); - } + logger.connection.info('Disabled'); } } - /* - https://api.slack.com/events/reaction_added - */ - onSlackReactionAdded(slackReactionMsg) { - if (slackReactionMsg) { - const rocketUser = this.getRocketUser(slackReactionMsg.user); - - if (rocketUser.roles.includes('bot')) { - return; - } - - //Lets find our Rocket originated message - let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); - - if (!rocketMsg) { - //Must have originated from Slack - const rocketID = this.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); - rocketMsg = RocketChat.models.Messages.findOneById(rocketID); - } - - if (rocketMsg && rocketUser) { - const rocketReaction = ':' + slackReactionMsg.reaction + ':'; + processSettings() { - //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.reactionsMap.set('set'+rocketMsg._id+rocketReaction, rocketUser); - logger.class.debug('Adding reaction from Slack'); - Meteor.runAsUser(rocketUser._id, () => { - Meteor.call('setReaction', rocketReaction, rocketMsg._id); - }); - } - } - } - - /** - * We have received a message from slack and we need to save/delete/update it into rocket - * https://api.slack.com/events/message - */ - onSlackMessage(slackMessage, isImporting) { - if (slackMessage.subtype) { - switch (slackMessage.subtype) { - case 'message_deleted': - this.processSlackMessageDeleted(slackMessage); - break; - case 'message_changed': - this.processSlackMessageChanged(slackMessage); - break; - default: - //Keeping backwards compatability for now, refactor later - this.processSlackNewMessage(slackMessage, isImporting); - } - } else { - //Simple message - this.processSlackNewMessage(slackMessage, isImporting); - } - } - - processSlackSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) { - let rocketMsgObj = null; - switch (slackMessage.subtype) { - case 'bot_message': - if (slackMessage.username !== undefined && this.excludeBotnames && slackMessage.username.match(this.excludeBotnames)) { - return; - } - - rocketMsgObj = { - msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), - rid: rocketChannel._id, - bot: true, - attachments: slackMessage.attachments, - username: slackMessage.username || slackMessage.bot_id - }; - this.addAliasToRocketMsg(slackMessage.username || slackMessage.bot_id, rocketMsgObj); - if (slackMessage.icons) { - rocketMsgObj.emoji = slackMessage.icons.emoji; - } - return rocketMsgObj; - case 'me_message': - return this.addAliasToRocketMsg(rocketUser.username, { - msg: `_${this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text)}_` - }); - case 'channel_join': - if (isImporting) { - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.addUserToRoom(rocketChannel._id, rocketUser); - } - return; - case 'group_join': - if (slackMessage.inviter) { - const inviter = slackMessage.inviter ? this.findRocketUser(slackMessage.inviter) || this.addRocketUser(slackMessage.inviter) : null; - if (isImporting) { - RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, { - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), - u: { - _id: inviter._id, - username: inviter.username - }, - imported: 'slackbridge' - }); - } else { - RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter); - } - } - return; - case 'channel_leave': - case 'group_leave': - if (isImporting) { - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, { - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), - imported: 'slackbridge' - }); - } else { - RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser); - } - return; - case 'channel_topic': - case 'group_topic': - if (isImporting) { - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false); - } - return; - case 'channel_purpose': - case 'group_purpose': - if (isImporting) { - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false); - } - return; - case 'channel_name': - case 'group_name': - if (isImporting) { - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false); - } - return; - case 'channel_archive': - case 'group_archive': - if (!isImporting) { - RocketChat.archiveRoom(rocketChannel); - } - return; - case 'channel_unarchive': - case 'group_unarchive': - if (!isImporting) { - RocketChat.unarchiveRoom(rocketChannel); - } - return; - case 'file_share': - 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); - } - break; - case 'file_comment': - logger.class.error('File comment not implemented'); - return; - case 'file_mention': - logger.class.error('File mentioned not implemented'); - return; - case 'pinned_item': - if (slackMessage.attachments && slackMessage.attachments[0] && slackMessage.attachments[0].text) { - rocketMsgObj = { - rid: rocketChannel._id, - t: 'message_pinned', - msg: '', - u: { - _id: rocketUser._id, - username: rocketUser.username - }, - attachments: [{ - 'text' : this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text), - 'author_name' : slackMessage.attachments[0].author_subname, - 'author_icon' : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname), - 'ts' : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000) - }] - }; - - if (!isImporting) { - RocketChat.models.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; - } else { - logger.class.error('Pinned item with no attachment'); + //Slack installation API token + RocketChat.settings.get('SlackBridge_APIToken', (key, value) => { + if (value !== this.apiToken) { + this.apiToken = value; + if (this.connected) { + this.disconnect(); + this.connect(); } - return; - case 'unpinned_item': - logger.class.error('Unpinned item not implemented'); - return; - } - } - - /** - 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); - uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) { - const url = Npm.require('url'); - const requestModule = /https/i.test(slackFileURL) ? Npm.require('https') : Npm.require('http'); - var parsedUrl = url.parse(slackFileURL, true); - parsedUrl.headers = { 'Authorization': 'Bearer ' + this.apiToken }; - requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => { - const fileId = Meteor.fileStore.create(details); - if (fileId) { - Meteor.fileStore.write(stream, fileId, (err, file) => { - console.log('fileStore.write', file); - if (err) { - throw new Error(err); - } else { - const url = file.url.replace(Meteor.absoluteUrl(), '/'); - const attachment = { - title: `File Uploaded: ${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; - } - - return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); - } - }); } - })); - } - - registerForRocketEvents() { - RocketChat.callbacks.add('afterSaveMessage', this.onRocketMessage.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Out'); - RocketChat.callbacks.add('afterDeleteMessage', this.onRocketMessageDelete.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Delete'); - RocketChat.callbacks.add('setReaction', this.onRocketSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_SetReaction'); - RocketChat.callbacks.add('unsetReaction', this.onRocketUnSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); - } - - unregisterForRocketEvents() { - RocketChat.callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); - RocketChat.callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); - RocketChat.callbacks.remove('setReaction', 'SlackBridge_SetReaction'); - RocketChat.callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction'); - } - registerForSlackEvents() { - var CLIENT_EVENTS = this.slackClient.CLIENT_EVENTS; - this.rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, () => { - logger.connection.info('Connected to Slack'); + logger.class.debug('Setting: ' + key, value); }); - this.rtm.on(CLIENT_EVENTS.RTM.UNABLE_TO_RTM_START, () => { - this.disconnect(); + //Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used. + RocketChat.settings.get('SlackBridge_AliasFormat', (key, value) => { + this.aliasFormat = value; + logger.class.debug('Setting: ' + key, value); }); - this.rtm.on(CLIENT_EVENTS.RTM.DISCONNECT, () => { - this.disconnect(); + //Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated. + RocketChat.settings.get('SlackBridge_ExcludeBotnames', (key, value) => { + this.excludeBotnames = value; + logger.class.debug('Setting: ' + key, value); }); - var RTM_EVENTS = this.slackClient.RTM_EVENTS; - - /** - * 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(RTM_EVENTS.MESSAGE, Meteor.bindEnvironment((slackMessage) => { - logger.events.debug('OnSlackEvent-MESSAGE: ', slackMessage); - if (slackMessage) { - this.onSlackMessage(slackMessage); - } - })); - - this.rtm.on(RTM_EVENTS.REACTION_ADDED, Meteor.bindEnvironment((reactionMsg) => { - logger.events.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); - if (reactionMsg) { - this.onSlackReactionAdded(reactionMsg); - } - })); - - this.rtm.on(RTM_EVENTS.REACTION_REMOVED, Meteor.bindEnvironment((reactionMsg) => { - logger.events.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); - if (reactionMsg) { - this.onSlackReactionRemoved(reactionMsg); - } - })); - - /** - * 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(RTM_EVENTS.CHANNEL_CREATED, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.CHANNEL_JOINED, Meteor.bindEnvironment(() => {})); - - /** - * Event fired when the bot leaves (or is removed from) a public channel - * { - * type: 'channel_left', - * channel: [channel_id] - * } - **/ - this.rtm.on(RTM_EVENTS.CHANNEL_LEFT, Meteor.bindEnvironment(() => {})); - - /** - * Event fired when an archived channel is deleted by an admin - * { - * type: 'channel_deleted', - * channel: [channel_id], - * event_ts: [ts.milli] - * } - **/ - this.rtm.on(RTM_EVENTS.CHANNEL_DELETED, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.CHANNEL_RENAME, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.GROUP_JOINED, Meteor.bindEnvironment(() => {})); - - /** - * Event fired when the bot leaves (or is removed from) a private channel - * { - * type: 'group_left', - * channel: [channel_id] - * } - **/ - this.rtm.on(RTM_EVENTS.GROUP_LEFT, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.GROUP_RENAME, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.TEAM_JOIN, Meteor.bindEnvironment(() => {})); - } - - findSlackChannel(rocketChannelName) { - logger.class.debug('Searching for Slack channel or group', rocketChannelName); - let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { - for (const channel of response.data.channels) { - if (channel.name === rocketChannelName && channel.is_member === true) { - return channel; - } - } - } - response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { - for (const group of response.data.groups) { - if (group.name === rocketChannelName) { - return group; - } - } - } - } - - importFromHistory(family, options) { - logger.class.debug('Importing messages history'); - const response = HTTP.get('https://slack.com/api/' + family + '.history', { params: _.extend({ token: this.apiToken }, options) }); - if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) { - let latest = 0; - for (const message of response.data.messages.reverse()) { - logger.class.debug('MESSAGE: ', message); - if (!latest || message.ts > latest) { - latest = message.ts; - } - message.channel = options.channel; - this.onSlackMessage(message, true); - } - return { has_more: response.data.has_more, ts: latest }; - } - } - - copySlackChannelInfo(rid, channelMap) { - logger.class.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); - const response = HTTP.get('https://slack.com/api/' + channelMap.family + '.info', { params: { token: this.apiToken, channel: channelMap.id } }); - if (response && response.data) { - const data = channelMap.family === 'channels' ? response.data.channel : response.data.group; - if (data && _.isArray(data.members) && data.members.length > 0) { - for (const member of data.members) { - const user = this.findRocketUser(member) || this.addRocketUser(member); - if (user) { - logger.class.debug('Adding user to room', user.username, rid); - RocketChat.addUserToRoom(rid, user, null, true); - } - } - } - - let topic = ''; - let topic_last_set = 0; - let topic_creator = null; - if (data && data.topic && data.topic.value) { - topic = data.topic.value; - topic_last_set = data.topic.last_set; - topic_creator = data.topic.creator; - } - - if (data && data.purpose && data.purpose.value) { - if (topic_last_set) { - if (topic_last_set < data.purpose.last_set) { - topic = data.purpose.topic; - topic_creator = data.purpose.creator; - } - } else { - topic = data.purpose.topic; - topic_creator = data.purpose.creator; - } - } - - if (topic) { - const creator = this.findRocketUser(topic_creator) || this.addRocketUser(topic_creator); - logger.class.debug('Setting room topic', rid, topic, creator.username); - RocketChat.saveRoomTopic(rid, topic, creator, false); - } - } - } - - copyPins(rid, channelMap) { - const response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } }); - if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) { - for (const pin of response.data.items) { - if (pin.message) { - const user = this.findRocketUser(pin.message.user); - const msgObj = { - rid: rid, - t: 'message_pinned', - msg: '', - u: { - _id: user._id, - username: user.username - }, - attachments: [{ - 'text' : this.convertSlackMsgTxtToRocketTxtFormat(pin.message.text), - 'author_name' : user.username, - 'author_icon' : getAvatarUrlFromUsername(user.username), - 'ts' : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000) - }] - }; - - RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000)); - } - } - } - } + //Choose whether SlackBridge should also send your messages back to Slack + RocketChat.settings.get('SlackBridge_Out_Enabled', (key, value) => { + logger.class.debug('Setting: ' + key, value); + }); - importMessages(rid, callback) { - logger.class.info('importMessages: ', rid); - const rocketchat_room = RocketChat.models.Rooms.findOneById(rid); - if (rocketchat_room) { - if (this.slackChannelMap[rid]) { - this.copySlackChannelInfo(rid, this.slackChannelMap[rid]); + //Send messages from all channels that exist in Slack and the bot has joined + RocketChat.settings.get('SlackBridge_Out_All', (key, value) => { + logger.class.debug('Setting: ' + key, value); + }); - logger.class.debug('Importing messages from Slack to Rocket.Chat', this.slackChannelMap[rid], rid); - let results = this.importFromHistory(this.slackChannelMap[rid].family, { channel: this.slackChannelMap[rid].id, oldest: 1 }); - while (results && results.has_more) { - results = this.importFromHistory(this.slackChannelMap[rid].family, { channel: this.slackChannelMap[rid].id, oldest: results.ts }); - } + //Choose which channels will send messages back to Slack + RocketChat.settings.get('SlackBridge_Out_Channels', (key, value) => { + logger.class.debug('Setting: ' + key, value); + }); - logger.class.debug('Pinning Slack channel messages to Rocket.Chat', this.slackChannelMap[rid], rid); - this.copyPins(rid, this.slackChannelMap[rid]); - return callback(); + //Is this entire SlackBridge enabled + RocketChat.settings.get('SlackBridge_Enabled', (key, value) => { + if (value && this.apiToken) { + this.connect(); } else { - const slack_room = this.findSlackChannel(rocketchat_room.name); - if (slack_room) { - this.slackChannelMap[rid] = { id: slack_room.id, family: slack_room.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - return this.importMessages(rid, callback); - } else { - logger.class.error('Could not find Slack room with specified name', rocketchat_room.name); - return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name')); - } - } - } else { - logger.class.error('Could not find Rocket.Chat room with specified id', rid); - return callback(new Meteor.Error('error-invalid-room', 'Invalid room')); - } - } - - populateSlackChannelMap() { - logger.class.debug('Populating channel map'); - let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { - for (const slackChannel of response.data.channels) { - const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } }); - if (rocketchat_room) { - this.slackChannelMap[rocketchat_room._id] = { id: slackChannel.id, family: slackChannel.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - } - } - } - response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { - for (const slackGroup of response.data.groups) { - const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } }); - if (rocketchat_room) { - this.slackChannelMap[rocketchat_room._id] = { id: slackGroup.id, family: slackGroup.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - } - } - } - } - - onRocketMessageDelete(rocketMessageDeleted) { - logger.class.debug('onRocketMessageDelete', rocketMessageDeleted); - - this.postDeleteMessageToSlack(rocketMessageDeleted); - } - - onRocketSetReaction(rocketMsgID, reaction) { - logger.class.debug('onRocketSetReaction'); - - if (rocketMsgID && reaction) { - if (this.reactionsMap.delete('set'+rocketMsgID+reaction)) { - //This was a Slack reaction, we don't need to tell Slack about it - return; - } - const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); - if (rocketMsg) { - const slackChannel = this.slackChannelMap[rocketMsg.rid].id; - const slackTS = this.getSlackTS(rocketMsg); - this.postReactionAddedToSlack(reaction.replace(/:/g, ''), slackChannel, slackTS); - } - } - } - - onRocketUnSetReaction(rocketMsgID, reaction) { - logger.class.debug('onRocketUnSetReaction'); - - if (rocketMsgID && reaction) { - if (this.reactionsMap.delete('unset'+rocketMsgID+reaction)) { - //This was a Slack unset reaction, we don't need to tell Slack about it - return; - } - - const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); - if (rocketMsg) { - const slackChannel = this.slackChannelMap[rocketMsg.rid].id; - const slackTS = this.getSlackTS(rocketMsg); - this.postReactionRemoveToSlack(reaction.replace(/:/g, ''), slackChannel, slackTS); - } - } - } - - onRocketMessage(rocketMessage) { - logger.class.debug('onRocketMessage', rocketMessage); - - if (rocketMessage.editedAt) { - //This is an Edit Event - this.processRocketMessageChanged(rocketMessage); - return rocketMessage; - } - // Ignore messages originating from Slack - if (rocketMessage._id.indexOf('slack-') === 0) { - return rocketMessage; - } - - //Probably a new message from Rocket.Chat - const outSlackChannels = RocketChat.settings.get('SlackBridge_Out_All') ? _.keys(this.slackChannelMap) : _.pluck(RocketChat.settings.get('SlackBridge_Out_Channels'), '_id') || []; - //logger.class.debug('Out SlackChannels: ', outSlackChannels); - if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) { - this.postMessageToSlack(this.slackChannelMap[rocketMessage.rid], rocketMessage); - } - return rocketMessage; - } - - /* - https://api.slack.com/methods/reactions.add - */ - postReactionAddedToSlack(reaction, slackChannel, slackTS) { - if (reaction && slackChannel && slackTS) { - const data = { - token: this.apiToken, - name: reaction, - channel: slackChannel, - timestamp: slackTS - }; - - logger.class.debug('Posting Add Reaction to Slack'); - const postResult = HTTP.post('https://slack.com/api/reactions.add', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Reaction added to Slack'); - } - } - } - - /* - https://api.slack.com/methods/reactions.remove - */ - postReactionRemoveToSlack(reaction, slackChannel, slackTS) { - if (reaction && slackChannel && slackTS) { - const data = { - token: this.apiToken, - name: reaction, - channel: slackChannel, - timestamp: slackTS - }; - - logger.class.debug('Posting Remove Reaction to Slack'); - const postResult = HTTP.post('https://slack.com/api/reactions.remove', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Reaction removed from Slack'); - } - } - } - - postDeleteMessageToSlack(rocketMessage) { - if (rocketMessage) { - const data = { - token: this.apiToken, - ts: this.getSlackTS(rocketMessage), - channel: this.slackChannelMap[rocketMessage.rid].id, - as_user: true - }; - - logger.class.debug('Post Delete Message to Slack', data); - const postResult = HTTP.post('https://slack.com/api/chat.delete', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Message deleted on Slack'); - } - } - } - - postMessageToSlack(slackChannel, rocketMessage) { - if (slackChannel && slackChannel.id) { - let iconUrl = getAvatarUrlFromUsername(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 - }; - logger.class.debug('Post Message To Slack', data); - const postResult = HTTP.post('https://slack.com/api/chat.postMessage', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { - RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); - logger.class.debug('RocketMsgID=' + rocketMessage._id + ' SlackMsgID=' + postResult.data.message.ts + ' SlackBotID=' + postResult.data.message.bot_id); - } - } - } - - /* - https://api.slack.com/methods/chat.update - */ - postMessageUpdateToSlack(slackChannel, rocketMessage) { - if (slackChannel && slackChannel.id) { - const data = { - token: this.apiToken, - ts: this.getSlackTS(rocketMessage), - channel: slackChannel.id, - text: rocketMessage.msg, - as_user: true - }; - logger.class.debug('Post UpdateMessage To Slack', data); - const postResult = HTTP.post('https://slack.com/api/chat.update', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Message updated on Slack'); - } - } - } - - processRocketMessageChanged(rocketMessage) { - if (rocketMessage) { - if (rocketMessage.updatedBySlack) { - //We have already processed this - delete rocketMessage.updatedBySlack; - return; - } - - //This was a change from Rocket.Chat - const slackChannel = this.slackChannelMap[rocketMessage.rid]; - this.postMessageUpdateToSlack(slackChannel, rocketMessage); - } - } - - /* - https://api.slack.com/events/message/message_deleted - */ - processSlackMessageDeleted(slackMessage) { - if (slackMessage.previous_message) { - const rocketChannel = this.getRocketChannel(slackMessage); - const rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); - - if (rocketChannel && rocketUser) { - //Find the Rocket message to delete - let rocketMsgObj = RocketChat.models.Messages - .findOneBySlackBotIdAndSlackTs(slackMessage.previous_message.bot_id, slackMessage.previous_message.ts); - - if (!rocketMsgObj) { - //Must have been a Slack originated msg - const _id = this.createRocketID(slackMessage.channel, slackMessage.previous_message.ts); - rocketMsgObj = RocketChat.models.Messages.findOneById(_id); - } - - if (rocketMsgObj) { - RocketChat.deleteMessage(rocketMsgObj, rocketUser); - logger.class.debug('Rocket message deleted by Slack'); - } - } - } - } - - /* - https://api.slack.com/events/message/message_changed - */ - processSlackMessageChanged(slackMessage) { - if (slackMessage.previous_message) { - const currentMsg = RocketChat.models.Messages.findOneById(this.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 = this.getRocketChannel(slackMessage); - const rocketUser = slackMessage.previous_message.user ? this.findRocketUser(slackMessage.previous_message.user) || this.addRocketUser(slackMessage.previous_message.user) : null; - - const rocketMsgObj = { - //@TODO _id - _id: this.createRocketID(slackMessage.channel, slackMessage.previous_message.ts), - rid: rocketChannel._id, - msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.message.text), - updatedBySlack: true //We don't want to notify slack about this change since Slack initiated it - }; - - RocketChat.updateMessage(rocketMsgObj, rocketUser); - logger.class.debug('Rocket message updated by Slack'); - } - } - } - - /* - This method will get refactored and broken down into single responsibilities - */ - processSlackNewMessage(slackMessage, isImporting) { - const rocketChannel = this.getRocketChannel(slackMessage); - let rocketUser = null; - if (slackMessage.subtype === 'bot_message') { - rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); - } else { - rocketUser = slackMessage.user ? this.findRocketUser(slackMessage.user) || this.addRocketUser(slackMessage.user) : null; - } - if (rocketChannel && rocketUser) { - const msgDataDefaults = { - _id: this.createRocketID(slackMessage.channel, slackMessage.ts), - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000) - }; - if (isImporting) { - msgDataDefaults['imported'] = 'slackbridge'; - } - try { - this.createAndSaveRocketMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting); - } catch (e) { - // http://www.mongodb.org/about/contributors/error-codes/ - // 11000 == duplicate key error - if (e.name === 'MongoError' && e.code === 11000) { - return; - } - - throw e; + this.disconnect(); } - } - } - - /** - * 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 - */ - getSlackTS(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; - } - - getRocketChannel(slackMessage) { - return slackMessage.channel ? this.findRocketChannel(slackMessage.channel) || this.addRocketChannel(slackMessage.channel) : null; - } - - getRocketUser(slackUser) { - return slackUser ? this.findRocketUser(slackUser) || this.addRocketUser(slackUser) : null; - } - - createRocketID(slackChannel, ts) { - return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`; + logger.class.debug('Setting: ' + key, value); + }); } - } RocketChat.SlackBridge = new SlackBridge; diff --git a/packages/rocketchat-slackbridge/tests/manual-tests.txt b/packages/rocketchat-slackbridge/tests/manual-tests.txt new file mode 100644 index 000000000000..a978998a57d6 --- /dev/null +++ b/packages/rocketchat-slackbridge/tests/manual-tests.txt @@ -0,0 +1,58 @@ +Config: + + + +Sending Message +* Send slack msg to rocket +* Send rocket msg to slack +* Repeat on a channel that rocketbot isn't a member + + +Edit Message: +* Edit slack msg from slack +* Edit rocket msg from rocket +* Edit slack msg from rocket - Expected fail, slack not updated (slack says can't update message) +* Edit rocket msg from slack - Expected fail, can't even edit +* Repeat on a channel that rocketbot isn't a member + + +Delete Message: +* Delete slack message in slack +* Delete rocket message in rocket +* Delete rocket message in slack +* Delete slack message in rocket - Expected fail, slack not updated, deleted in rocket +* Repeat on a channel that rocketbot isn't a member + + +React to a Message: +* React to a slack message in slack +* React to a rocket message in slack +* React to a rocket message in rocket +* React to a slack message in rocket +* Repeat on a channel that rocketbot isn't a member + + +Channels: +Channel exists on both, but no rocketbot. +* Add rocket bot to slack, send messages from both + +Channel exists on rocket only +* Create slack channel, add rocket bot: +** Send message from rocket +** Send message from slack + +Channel exists on slack only +* Add rocket bot to slack: +** Send message from slack +** Send message from rocket + +Channel exists on both w/ rocketbot +* Remove rocket channel, send messages from slack (same as ch exists on slack only) +* Remove rocketbot from slack: +** Send message from slack +** Send message from rocket + + + + + From a6c5a60ec593c1f69de61b3d6acbaec99cf6a6a1 Mon Sep 17 00:00:00 2001 From: kablewi Date: Tue, 7 Mar 2017 16:26:30 -0800 Subject: [PATCH 02/11] Support for dynamic slack and rocket channels --- packages/rocketchat-slackbridge/README.md | 2 +- packages/rocketchat-slackbridge/logger.js | 4 +- packages/rocketchat-slackbridge/package.js | 5 + packages/rocketchat-slackbridge/rocket.js | 422 +++++ packages/rocketchat-slackbridge/slack.js | 1085 +++++++++++++ .../rocketchat-slackbridge/slackbridge.js | 1381 +---------------- .../tests/manual-tests.txt | 58 + 7 files changed, 1630 insertions(+), 1327 deletions(-) create mode 100644 packages/rocketchat-slackbridge/rocket.js create mode 100644 packages/rocketchat-slackbridge/slack.js create mode 100644 packages/rocketchat-slackbridge/tests/manual-tests.txt diff --git a/packages/rocketchat-slackbridge/README.md b/packages/rocketchat-slackbridge/README.md index 5ce00d638c50..babd07e43f09 100644 --- a/packages/rocketchat-slackbridge/README.md +++ b/packages/rocketchat-slackbridge/README.md @@ -27,7 +27,7 @@ The following can be configured in your Rocket.Chat Administration SlackBridge p #### Group Chat Messages * Send and receive basic messages -* Delete messages +* Delete messages (Can't delete slack message from rocket) * Edit messages (Slack doesn't allow editing of BOT messages, so can't edit a Rocket msg in Slack) * React to messages (as BOT in Slack) diff --git a/packages/rocketchat-slackbridge/logger.js b/packages/rocketchat-slackbridge/logger.js index a64654f619d2..eac5c2ae6ffc 100644 --- a/packages/rocketchat-slackbridge/logger.js +++ b/packages/rocketchat-slackbridge/logger.js @@ -5,6 +5,8 @@ logger = new Logger('SlackBridge', { sections: { connection: 'Connection', events: 'Events', - class: 'Class' + class: 'Class', + slack: 'Slack', + rocket: 'Rocket' } }); diff --git a/packages/rocketchat-slackbridge/package.js b/packages/rocketchat-slackbridge/package.js index fc18d4da1b77..9999c175905a 100644 --- a/packages/rocketchat-slackbridge/package.js +++ b/packages/rocketchat-slackbridge/package.js @@ -15,8 +15,13 @@ Package.onUse(function(api) { api.addFiles('logger.js', 'server'); api.addFiles('settings.js', 'server'); + api.addFiles('rocket.js', 'server'); + api.addFiles('slack.js', 'server'); api.addFiles('slackbridge.js', 'server'); api.addFiles('slashcommand/slackbridge_import.server.js', 'server'); + + api.export('SB_SlackAdapter', 'server'); + api.export('SB_RocketAdapter', 'server'); }); Npm.depends({ diff --git a/packages/rocketchat-slackbridge/rocket.js b/packages/rocketchat-slackbridge/rocket.js new file mode 100644 index 000000000000..bd97eb33d073 --- /dev/null +++ b/packages/rocketchat-slackbridge/rocket.js @@ -0,0 +1,422 @@ +/* globals logger SB_RocketAdapter */ +/* exported SB_RocketAdapter */ + +class RocketAdapter { + constructor(slackBridge) { + logger.rocket.debug('constructor'); + this.slackBridge = slackBridge; + this.util = Npm.require('util'); + + this.userTags = {}; + this.slack = {}; + } + + connect() { + this.registerForEvents(); + } + + disconnect() { + this.unregisterForEvents(); + } + + setSlack(slack) { + this.slack = slack; + } + + registerForEvents() { + logger.rocket.debug('Register for events'); + RocketChat.callbacks.add('afterSaveMessage', this.onMessage.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Out'); + RocketChat.callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Delete'); + RocketChat.callbacks.add('setReaction', this.onSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_SetReaction'); + RocketChat.callbacks.add('unsetReaction', this.onUnSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); + } + + unregisterForEvents() { + logger.rocket.debug('Unregister for events'); + RocketChat.callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); + RocketChat.callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); + RocketChat.callbacks.remove('setReaction', 'SlackBridge_SetReaction'); + RocketChat.callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction'); + } + + onMessageDelete(rocketMessageDeleted) { + try { + if (! this.slack.getSlackChannel(rocketMessageDeleted.rid)) { + //This is on a channel that the rocket bot is not subscribed + return; + } + logger.rocket.debug('onRocketMessageDelete', rocketMessageDeleted); + + this.slack.postDeleteMessage(rocketMessageDeleted); + } catch (err) { + logger.rocket.error('Unhandled error onMessageDelete', err); + } + } + + onSetReaction(rocketMsgID, reaction) { + try { + logger.rocket.debug('onRocketSetReaction'); + + if (rocketMsgID && reaction) { + if (this.slackBridge.reactionsMap.delete('set' + rocketMsgID + reaction)) { + //This was a Slack reaction, we don't need to tell Slack about it + return; + } + const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); + if (rocketMsg) { + const slackChannel = this.slack.getSlackChannel(rocketMsg.rid); + if (null != slackChannel) { + const slackTS = this.slack.getTimeStamp(rocketMsg); + this.slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS); + } + } + } + } catch (err) { + logger.rocket.error('Unhandled error onSetReaction', err); + } + } + + onUnSetReaction(rocketMsgID, reaction) { + try { + logger.rocket.debug('onRocketUnSetReaction'); + + if (rocketMsgID && reaction) { + if (this.slackBridge.reactionsMap.delete('unset' + rocketMsgID + reaction)) { + //This was a Slack unset reaction, we don't need to tell Slack about it + return; + } + + const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); + if (rocketMsg) { + const slackChannel = this.slack.getSlackChannel(rocketMsg.rid); + if (null != slackChannel) { + const slackTS = this.slack.getTimeStamp(rocketMsg); + this.slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS); + } + } + } + } catch (err) { + logger.rocket.error('Unhandled error onUnSetReaction', err); + } + } + + onMessage(rocketMessage) { + try { + if (! this.slack.getSlackChannel(rocketMessage.rid)) { + //This is on a channel that the rocket bot is not subscribed + return; + } + logger.rocket.debug('onRocketMessage', rocketMessage); + + if (rocketMessage.editedAt) { + //This is an Edit Event + this.processMessageChanged(rocketMessage); + return rocketMessage; + } + // Ignore messages originating from Slack + if (rocketMessage._id.indexOf('slack-') === 0) { + return rocketMessage; + } + + //A new message from Rocket.Chat + this.processSendMessage(rocketMessage); + } catch (err) { + logger.rocket.error('Unhandled error onMessage', err); + } + + return rocketMessage; + } + + processSendMessage(rocketMessage) { + //Since we got this message, SlackBridge_Out_Enabled is true + + if (RocketChat.settings.get('SlackBridge_Out_All') === true) { + this.slack.postMessage(this.slack.getSlackChannel(rocketMessage.rid), rocketMessage); + } else { + //They want to limit to certain groups + const outSlackChannels = _.pluck(RocketChat.settings.get('SlackBridge_Out_Channels'), '_id') || []; + //logger.rocket.debug('Out SlackChannels: ', outSlackChannels); + if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) { + this.slack.postMessage(this.slack.getSlackChannel(rocketMessage.rid), rocketMessage); + } + } + } + + processMessageChanged(rocketMessage) { + if (rocketMessage) { + if (rocketMessage.updatedBySlack) { + //We have already processed this + delete rocketMessage.updatedBySlack; + return; + } + + //This was a change from Rocket.Chat + const slackChannel = this.slack.getSlackChannel(rocketMessage.rid); + this.slack.postMessageUpdate(slackChannel, rocketMessage); + } + } + + + getChannel(slackMessage) { + return slackMessage.channel ? this.findChannel(slackMessage.channel) || this.addChannel(slackMessage.channel) : null; + } + + getUser(slackUser) { + return slackUser ? this.findUser(slackUser) || this.addUser(slackUser) : null; + } + + createRocketID(slackChannel, ts) { + return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`; + } + + findChannel(slackChannelId) { + return RocketChat.models.Rooms.findOneByImportId(slackChannelId); + } + + addChannel(slackChannelID, hasRetried = false) { + logger.rocket.debug('Adding Rocket.Chat channel from Slack', slackChannelID); + let slackResults = null; + let isGroup = false; + if (slackChannelID.charAt(0) === 'C') { + slackResults = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.slackBridge.apiToken, channel: slackChannelID } }); + } else if (slackChannelID.charAt(0) === 'G') { + slackResults = HTTP.get('https://slack.com/api/groups.info', { params: { token: this.slackBridge.apiToken, channel: slackChannelID } }); + isGroup = true; + } + if (slackResults && slackResults.data && slackResults.data.ok === true) { + const rocketChannelData = isGroup ? slackResults.data.group : slackResults.data.channel; + const existingRocketRoom = RocketChat.models.Rooms.findOneByName(rocketChannelData.name); + + // If the room exists, make sure we have its id in importIds + if (existingRocketRoom || rocketChannelData.is_general) { + rocketChannelData.rocketId = rocketChannelData.is_general ? 'GENERAL' : existingRocketRoom._id; + RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); + } else { + const rocketUsers = []; + for (const member of rocketChannelData.members) { + if (member !== rocketChannelData.creator) { + const rocketUser = this.findUser(member) || this.addUser(member); + if (rocketUser && rocketUser.username) { + rocketUsers.push(rocketUser.username); + } + } + } + const rocketUserCreator = rocketChannelData.creator ? this.findUser(rocketChannelData.creator) || this.addUser(rocketChannelData.creator) : null; + if (!rocketUserCreator) { + logger.rocket.error('Could not fetch room creator information', rocketChannelData.creator); + return; + } + + try { + const rocketChannel = RocketChat.createRoom(isGroup ? 'p' : 'c', rocketChannelData.name, rocketUserCreator.username, rocketUsers); + rocketChannelData.rocketId = rocketChannel.rid; + } catch (e) { + if (!hasRetried) { + logger.rocket.debug('Error adding channel from Slack. Will retry in 1s.', e.message); + // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. + Meteor._sleepForMs(1000); + return this.findChannel(slackChannelID) || this.addChannel(slackChannelID, true); + } else { + console.log(e.message); + } + } + + const roomUpdate = { + ts: new Date(rocketChannelData.created * 1000) + }; + let lastSetTopic = 0; + if (!_.isEmpty(rocketChannelData.topic && rocketChannelData.topic.value)) { + roomUpdate.topic = rocketChannelData.topic.value; + lastSetTopic = rocketChannelData.topic.last_set; + } + if (!_.isEmpty(rocketChannelData.purpose && rocketChannelData.purpose.value) && rocketChannelData.purpose.last_set > lastSetTopic) { + roomUpdate.topic = rocketChannelData.purpose.value; + } + RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); + this.slack.addSlackChannel(rocketChannelData.rocketId, slackChannelID); + } + return RocketChat.models.Rooms.findOneById(rocketChannelData.rocketId); + } + logger.rocket.debug('Channel not added'); + return; + } + + findUser(slackUserID) { + const rocketUser = RocketChat.models.Users.findOneByImportId(slackUserID); + if (rocketUser && !this.userTags[slackUserID]) { + this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUser.username}` }; + } + return rocketUser; + } + + addUser(slackUserID) { + logger.rocket.debug('Adding Rocket.Chat user from Slack', slackUserID); + const slackResults = HTTP.get('https://slack.com/api/users.info', { params: { token: this.slackBridge.apiToken, user: slackUserID } }); + if (slackResults && slackResults.data && slackResults.data.ok === true && slackResults.data.user) { + const rocketUserData = slackResults.data.user; + const isBot = rocketUserData.is_bot === true; + const email = rocketUserData.profile && rocketUserData.profile.email || ''; + let existingRocketUser; + if (!isBot) { + existingRocketUser = RocketChat.models.Users.findOneByEmailAddress(email) || RocketChat.models.Users.findOneByUsername(rocketUserData.name); + } else { + existingRocketUser = RocketChat.models.Users.findOneByUsername(rocketUserData.name); + } + + if (existingRocketUser) { + rocketUserData.rocketId = existingRocketUser._id; + rocketUserData.name = existingRocketUser.username; + } else { + const newUser = { + password: Random.id(), + username: rocketUserData.name + }; + + if (!isBot && email) { + newUser.email = email; + } + + if (isBot) { + newUser.joinDefaultChannels = false; + } + + rocketUserData.rocketId = Accounts.createUser(newUser); + const userUpdate = { + utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, + roles: isBot ? [ 'bot' ] : [ 'user' ] + }; + + if (rocketUserData.profile && rocketUserData.profile.real_name) { + userUpdate['name'] = rocketUserData.profile.real_name; + } + + if (rocketUserData.deleted) { + userUpdate['active'] = false; + userUpdate['services.resume.loginTokens'] = []; + } + + RocketChat.models.Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate }); + + const user = RocketChat.models.Users.findOneById(rocketUserData.rocketId); + + let url = null; + if (rocketUserData.profile) { + if (rocketUserData.profile.image_original) { + url = rocketUserData.profile.image_original; + } else if (rocketUserData.profile.image_512) { + url = rocketUserData.profile.image_512; + } + } + if (url) { + try { + RocketChat.setUserAvatar(user, url, null, 'url'); + } catch (error) { + logger.rocket.debug('Error setting user avatar', error.message); + } + } + } + + const importIds = [ rocketUserData.id ]; + if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) { + importIds.push(rocketUserData.profile.bot_id); + } + RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); + if (!this.userTags[slackUserID]) { + this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUserData.name}` }; + } + return RocketChat.models.Users.findOneById(rocketUserData.rocketId); + } + logger.rocket.debug('User not added'); + return; + } + + addAliasToMsg(rocketUserName, rocketMsgObj) { + var aliasFormat = RocketChat.settings.get('SlackBridge_AliasFormat'); + if (aliasFormat) { + var alias = this.util.format(aliasFormat, rocketUserName); + + if (alias !== rocketUserName) { + rocketMsgObj.alias = alias; + } + } + + return rocketMsgObj; + } + + createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting) { + if (slackMessage.type === 'message') { + let rocketMsgObj = {}; + if (!_.isEmpty(slackMessage.subtype)) { + rocketMsgObj = this.slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting); + if (!rocketMsgObj) { + return; + } + } else { + rocketMsgObj = { + msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), + rid: rocketChannel._id, + u: { + _id: rocketUser._id, + username: rocketUser.username + } + }; + + this.addAliasToMsg(rocketUser.username, rocketMsgObj); + } + _.extend(rocketMsgObj, rocketMsgDataDefaults); + if (slackMessage.edited) { + rocketMsgObj.editedAt = new Date(parseInt(slackMessage.edited.ts.split('.')[0]) * 1000); + } + if (slackMessage.subtype === 'bot_message') { + rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); + } + + if (slackMessage.pinned_to && slackMessage.pinned_to.indexOf(slackMessage.channel) !== -1) { + rocketMsgObj.pinned = true; + rocketMsgObj.pinnedAt = Date.now; + rocketMsgObj.pinnedBy = _.pick(rocketUser, '_id', 'username'); + } + if (slackMessage.subtype === 'bot_message') { + Meteor.setTimeout(() => { + if (slackMessage.bot_id && slackMessage.ts && !RocketChat.models.Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) { + RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); + } + }, 500); + } else { + logger.rocket.debug('Send message to Rocket.Chat'); + RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); + } + } + } + + convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) { + if (!_.isEmpty(slackMsgTxt)) { + slackMsgTxt = slackMsgTxt.replace(//g, '@all'); + slackMsgTxt = slackMsgTxt.replace(//g, '@all'); + slackMsgTxt = slackMsgTxt.replace(/>/g, '<'); + slackMsgTxt = slackMsgTxt.replace(/</g, '>'); + slackMsgTxt = slackMsgTxt.replace(/&/g, '&'); + slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:'); + slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:'); + slackMsgTxt = slackMsgTxt.replace(/:piggy:/g, ':pig:'); + slackMsgTxt = slackMsgTxt.replace(/:uk:/g, ':gb:'); + slackMsgTxt = slackMsgTxt.replace(/<(http[s]?:[^>]*)>/g, '$1'); + + slackMsgTxt.replace(/(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g, (match, userId) => { + if (!this.userTags[userId]) { + this.findUser(userId) || this.addUser(userId); // This adds userTags for the userId + } + const userTags = this.userTags[userId]; + if (userTags) { + slackMsgTxt = slackMsgTxt.replace(userTags.slack, userTags.rocket); + } + }); + } else { + slackMsgTxt = ''; + } + return slackMsgTxt; + } + +} + +SB_RocketAdapter = RocketAdapter; diff --git a/packages/rocketchat-slackbridge/slack.js b/packages/rocketchat-slackbridge/slack.js new file mode 100644 index 000000000000..44b304531dc5 --- /dev/null +++ b/packages/rocketchat-slackbridge/slack.js @@ -0,0 +1,1085 @@ +/* globals logger SB_SlackAdapter */ +/* exported SB_SlackAdapter */ + +class SlackAdapter { + + constructor(slackBridge) { + logger.slack.debug('constructor'); + this.slackBridge = slackBridge; + this.slackClient = Npm.require('slack-client'); + 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 = {}; + } + + /** + * Connect to the remote Slack server using the passed in token API and register for Slack events + * @param apiToken + */ + connect(apiToken) { + this.apiToken = apiToken; + + var RtmClient = this.slackClient.RtmClient; + if (null != RtmClient) { + RtmClient.disconnect; + } + this.rtm = new RtmClient(this.apiToken); + this.rtm.start(); + this.registerForEvents(); + + Meteor.startup(() => { + try { + this.populateMembershipChannelMap(); // If run outside of Meteor.startup, HTTP is not defined + } catch (err) { + logger.slack.error('Error attempting to connect to Slack', err); + this.slackBridge.disconnect(); + } + }); + } + + /** + * Unregister for slack events and disconnect from Slack + */ + disconnect() { + this.rtm.disconnect && this.rtm.disconnect; + } + + setRocket(rocket) { + this.rocket = rocket; + } + + registerForEvents() { + logger.slack.debug('Register for events'); + var CLIENT_EVENTS = this.slackClient.CLIENT_EVENTS; + this.rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, () => { + logger.slack.info('Connected to Slack'); + }); + + this.rtm.on(CLIENT_EVENTS.RTM.UNABLE_TO_RTM_START, () => { + this.slackBridge.disconnect(); + }); + + this.rtm.on(CLIENT_EVENTS.RTM.DISCONNECT, () => { + logger.slack.info('Disconnected from Slack'); + this.slackBridge.disconnect(); + }); + + var RTM_EVENTS = this.slackClient.RTM_EVENTS; + + /** + * 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(RTM_EVENTS.MESSAGE, Meteor.bindEnvironment((slackMessage) => { + logger.slack.debug('OnSlackEvent-MESSAGE: ', slackMessage); + if (slackMessage) { + try { + this.onMessage(slackMessage); + } catch (err) { + logger.slack.error('Unhandled error onMessage', err); + } + } + })); + + this.rtm.on(RTM_EVENTS.REACTION_ADDED, Meteor.bindEnvironment((reactionMsg) => { + logger.slack.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); + if (reactionMsg) { + try { + this.onReactionAdded(reactionMsg); + } catch (err) { + logger.slack.error('Unhandled error onReactionAdded', err); + } + } + })); + + this.rtm.on(RTM_EVENTS.REACTION_REMOVED, Meteor.bindEnvironment((reactionMsg) => { + logger.slack.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); + if (reactionMsg) { + try { + this.onReactionRemoved(reactionMsg); + } catch (err) { + logger.slack.error('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(RTM_EVENTS.CHANNEL_CREATED, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.CHANNEL_JOINED, Meteor.bindEnvironment(() => {})); + + /** + * Event fired when the bot leaves (or is removed from) a public channel + * { + * type: 'channel_left', + * channel: [channel_id] + * } + **/ + this.rtm.on(RTM_EVENTS.CHANNEL_LEFT, Meteor.bindEnvironment((channelLeftMsg) => { + logger.slack.debug('OnSlackEvent-CHANNEL_LEFT: ', channelLeftMsg); + if (channelLeftMsg) { + try { + this.onChannelLeft(channelLeftMsg); + } catch (err) { + logger.slack.error('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(RTM_EVENTS.CHANNEL_DELETED, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.CHANNEL_RENAME, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.GROUP_JOINED, Meteor.bindEnvironment(() => {})); + + /** + * Event fired when the bot leaves (or is removed from) a private channel + * { + * type: 'group_left', + * channel: [channel_id] + * } + **/ + this.rtm.on(RTM_EVENTS.GROUP_LEFT, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.GROUP_RENAME, Meteor.bindEnvironment(() => {})); + + /** + * 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(RTM_EVENTS.TEAM_JOIN, Meteor.bindEnvironment(() => {})); + } + + /* + https://api.slack.com/events/reaction_removed + */ + onReactionRemoved(slackReactionMsg) { + if (slackReactionMsg) { + const rocketUser = this.rocket.getUser(slackReactionMsg.user); + //Lets find our Rocket originated message + let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); + + if (!rocketMsg) { + //Must have originated from Slack + const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); + rocketMsg = RocketChat.models.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); + logger.slack.debug('Removing reaction from Slack'); + Meteor.runAsUser(rocketUser._id, () => { + Meteor.call('setReaction', rocketReaction, rocketMsg._id); + }); + } + } + } + + /* + https://api.slack.com/events/reaction_added + */ + onReactionAdded(slackReactionMsg) { + if (slackReactionMsg) { + const rocketUser = this.rocket.getUser(slackReactionMsg.user); + + if (rocketUser.roles.includes('bot')) { + return; + } + + //Lets find our Rocket originated message + let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); + + if (!rocketMsg) { + //Must have originated from Slack + const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); + rocketMsg = RocketChat.models.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); + logger.slack.debug('Adding reaction from Slack'); + Meteor.runAsUser(rocketUser._id, () => { + Meteor.call('setReaction', 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 + */ + onMessage(slackMessage, isImporting) { + if (slackMessage.subtype) { + switch (slackMessage.subtype) { + case 'message_deleted': + this.processMessageDeleted(slackMessage); + break; + case 'message_changed': + this.processMessageChanged(slackMessage); + break; + case 'channel_join': + this.processChannelJoin(slackMessage); + break; + default: + //Keeping backwards compatability for now, refactor later + this.processNewMessage(slackMessage, isImporting); + } + } else { + //Simple message + this.processNewMessage(slackMessage, isImporting); + } + } + + postGetChannelInfo(slackChID) { + logger.slack.debug('Getting slack channel info', slackChID); + const response = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.apiToken, channel: slackChID } }); + if (response && response.data) { + return response.data.channel; + } + } + + postFindChannel(rocketChannelName) { + logger.slack.debug('Searching for Slack channel or group', rocketChannelName); + let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { + for (const channel of response.data.channels) { + if (channel.name === rocketChannelName && channel.is_member === true) { + return channel; + } + } + } + response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { + for (const group of response.data.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 (null == ch) { + 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); + } + + populateMembershipChannelMapByChannels() { + const response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { + for (const slackChannel of response.data.channels) { + const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } }); + if (rocketchat_room) { + if (slackChannel.is_member) { + this.addSlackChannel(rocketchat_room._id, slackChannel.id); + } + } + } + } + } + + populateMembershipChannelMapByGroups() { + const response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); + if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { + for (const slackGroup of response.data.groups) { + const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } }); + if (rocketchat_room) { + if (slackGroup.is_member) { + this.addSlackChannel(rocketchat_room._id, slackGroup.id); + } + } + } + } + } + + populateMembershipChannelMap() { + logger.slack.debug('Populating channel map'); + this.populateMembershipChannelMapByChannels(); + this.populateMembershipChannelMapByGroups(); + } + + /* + https://api.slack.com/methods/reactions.add + */ + postReactionAdded(reaction, slackChannel, slackTS) { + if (reaction && slackChannel && slackTS) { + const data = { + token: this.apiToken, + name: reaction, + channel: slackChannel, + timestamp: slackTS + }; + + logger.slack.debug('Posting Add Reaction to Slack'); + const postResult = HTTP.post('https://slack.com/api/reactions.add', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Reaction added to Slack'); + } + } + } + + /* + https://api.slack.com/methods/reactions.remove + */ + postReactionRemove(reaction, slackChannel, slackTS) { + if (reaction && slackChannel && slackTS) { + const data = { + token: this.apiToken, + name: reaction, + channel: slackChannel, + timestamp: slackTS + }; + + logger.slack.debug('Posting Remove Reaction to Slack'); + const postResult = HTTP.post('https://slack.com/api/reactions.remove', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Reaction removed from Slack'); + } + } + } + + postDeleteMessage(rocketMessage) { + if (rocketMessage) { + var slackChannel = this.getSlackChannel(rocketMessage.rid); + + if (null != slackChannel) { + const data = { + token: this.apiToken, + ts: this.getTimeStamp(rocketMessage), + channel: this.getSlackChannel(rocketMessage.rid).id, + as_user: true + }; + + logger.slack.debug('Post Delete Message to Slack', data); + const postResult = HTTP.post('https://slack.com/api/chat.delete', {params: data}); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Message deleted on Slack'); + } + } + } + } + + postMessage(slackChannel, rocketMessage) { + if (slackChannel && slackChannel.id) { + let iconUrl = getAvatarUrlFromUsername(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 + }; + logger.slack.debug('Post Message To Slack', data); + const postResult = HTTP.post('https://slack.com/api/chat.postMessage', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { + RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); + logger.slack.debug('RocketMsgID=' + rocketMessage._id + ' SlackMsgID=' + postResult.data.message.ts + ' SlackBotID=' + postResult.data.message.bot_id); + } + } + } + + /* + https://api.slack.com/methods/chat.update + */ + 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 + }; + logger.slack.debug('Post UpdateMessage To Slack', data); + const postResult = HTTP.post('https://slack.com/api/chat.update', { params: data }); + if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { + logger.slack.debug('Message updated on Slack'); + } + } + } + + processChannelJoin(slackMessage) { + logger.slack.debug('Channel join', slackMessage.channel.id); + const rocketCh = this.rocket.addChannel(slackMessage.channel); + if (null != rocketCh) { + this.addSlackChannel(rocketCh._id, slackMessage.channel); + } + } + + /* + https://api.slack.com/events/message/message_deleted + */ + processMessageDeleted(slackMessage) { + if (slackMessage.previous_message) { + const rocketChannel = this.rocket.getChannel(slackMessage); + const rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); + + if (rocketChannel && rocketUser) { + //Find the Rocket message to delete + let rocketMsgObj = RocketChat.models.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 = RocketChat.models.Messages.findOneById(_id); + } + + if (rocketMsgObj) { + RocketChat.deleteMessage(rocketMsgObj, rocketUser); + logger.slack.debug('Rocket message deleted by Slack'); + } + } + } + } + + /* + https://api.slack.com/events/message/message_changed + */ + processMessageChanged(slackMessage) { + if (slackMessage.previous_message) { + const currentMsg = RocketChat.models.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 = this.rocket.getChannel(slackMessage); + const rocketUser = slackMessage.previous_message.user ? this.rocket.findUser(slackMessage.previous_message.user) || 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: this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.message.text), + updatedBySlack: true //We don't want to notify slack about this change since Slack initiated it + }; + + RocketChat.updateMessage(rocketMsgObj, rocketUser); + logger.slack.debug('Rocket message updated by Slack'); + } + } + } + + /* + This method will get refactored and broken down into single responsibilities + */ + processNewMessage(slackMessage, isImporting) { + const rocketChannel = this.rocket.getChannel(slackMessage); + let rocketUser = null; + if (slackMessage.subtype === 'bot_message') { + rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); + } else { + rocketUser = slackMessage.user ? this.rocket.findUser(slackMessage.user) || 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 { + this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting); + } catch (e) { + // http://www.mongodb.org/about/contributors/error-codes/ + // 11000 == duplicate key error + if (e.name === 'MongoError' && e.code === 11000) { + return; + } + + throw e; + } + } + } + + + processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + let rocketMsgObj = null; + switch (slackMessage.subtype) { + case 'bot_message': + var excludeBotNames = RocketChat.settings.get('SlackBridge_Botnames'); + if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) { + return; + } + + rocketMsgObj = { + msg: 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; + case 'me_message': + return this.rocket.addAliasToMsg(rocketUser.username, { + msg: `_${this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text)}_` + }); + case 'channel_join': + if (isImporting) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.addUserToRoom(rocketChannel._id, rocketUser); + } + return; + case 'group_join': + if (slackMessage.inviter) { + const inviter = slackMessage.inviter ? this.rocket.findUser(slackMessage.inviter) || this.rocket.addUser(slackMessage.inviter) : null; + if (isImporting) { + RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, { + ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), + u: { + _id: inviter._id, + username: inviter.username + }, + imported: 'slackbridge' + }); + } else { + RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter); + } + } + return; + case 'channel_leave': + case 'group_leave': + if (isImporting) { + RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, { + ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), + imported: 'slackbridge' + }); + } else { + RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser); + } + return; + case 'channel_topic': + case 'group_topic': + if (isImporting) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false); + } + return; + case 'channel_purpose': + case 'group_purpose': + if (isImporting) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false); + } + return; + case 'channel_name': + case 'group_name': + if (isImporting) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false); + } + return; + case 'channel_archive': + case 'group_archive': + if (!isImporting) { + RocketChat.archiveRoom(rocketChannel); + } + return; + case 'channel_unarchive': + case 'group_unarchive': + if (!isImporting) { + RocketChat.unarchiveRoom(rocketChannel); + } + return; + case 'file_share': + 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); + } + break; + case 'file_comment': + logger.slack.error('File comment not implemented'); + return; + case 'file_mention': + logger.slack.error('File mentioned not implemented'); + return; + case 'pinned_item': + if (slackMessage.attachments && slackMessage.attachments[0] && slackMessage.attachments[0].text) { + rocketMsgObj = { + rid: rocketChannel._id, + t: 'message_pinned', + msg: '', + u: { + _id: rocketUser._id, + username: rocketUser.username + }, + attachments: [{ + 'text' : this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text), + 'author_name' : slackMessage.attachments[0].author_subname, + 'author_icon' : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname), + 'ts' : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000) + }] + }; + + if (!isImporting) { + RocketChat.models.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; + } else { + logger.slack.error('Pinned item with no attachment'); + } + return; + case 'unpinned_item': + logger.slack.error('Unpinned item not implemented'); + return; + } + } + + /** + 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); + uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) { + const url = Npm.require('url'); + const requestModule = /https/i.test(slackFileURL) ? Npm.require('https') : Npm.require('http'); + var parsedUrl = url.parse(slackFileURL, true); + parsedUrl.headers = { 'Authorization': 'Bearer ' + this.apiToken }; + requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => { + const fileId = Meteor.fileStore.create(details); + if (fileId) { + Meteor.fileStore.write(stream, fileId, (err, file) => { + console.log('fileStore.write', file); + if (err) { + throw new Error(err); + } else { + const url = file.url.replace(Meteor.absoluteUrl(), '/'); + const attachment = { + title: `File Uploaded: ${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; + } + + return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); + } + }); + } + })); + } + + importFromHistory(family, options) { + logger.slack.debug('Importing messages history'); + const response = HTTP.get('https://slack.com/api/' + family + '.history', { params: _.extend({ token: this.apiToken }, options) }); + if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) { + let latest = 0; + for (const message of response.data.messages.reverse()) { + logger.slack.debug('MESSAGE: ', message); + if (!latest || message.ts > latest) { + latest = message.ts; + } + message.channel = options.channel; + this.onMessage(message, true); + } + return { has_more: response.data.has_more, ts: latest }; + } + } + + copyChannelInfo(rid, channelMap) { + logger.slack.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); + const response = HTTP.get('https://slack.com/api/' + channelMap.family + '.info', { params: { token: this.apiToken, channel: channelMap.id } }); + if (response && response.data) { + const data = channelMap.family === 'channels' ? response.data.channel : response.data.group; + if (data && _.isArray(data.members) && data.members.length > 0) { + for (const member of data.members) { + const user = this.rocket.findUser(member) || this.rocket.addUser(member); + if (user) { + logger.slack.debug('Adding user to room', user.username, rid); + RocketChat.addUserToRoom(rid, user, null, true); + } + } + } + + let topic = ''; + let topic_last_set = 0; + let topic_creator = null; + if (data && data.topic && data.topic.value) { + topic = data.topic.value; + topic_last_set = data.topic.last_set; + topic_creator = data.topic.creator; + } + + if (data && data.purpose && data.purpose.value) { + if (topic_last_set) { + if (topic_last_set < data.purpose.last_set) { + topic = data.purpose.topic; + topic_creator = data.purpose.creator; + } + } else { + topic = data.purpose.topic; + topic_creator = data.purpose.creator; + } + } + + if (topic) { + const creator = this.rocket.findUser(topic_creator) || this.rocket.addUser(topic_creator); + logger.slack.debug('Setting room topic', rid, topic, creator.username); + RocketChat.saveRoomTopic(rid, topic, creator, false); + } + } + } + + copyPins(rid, channelMap) { + const response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } }); + if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) { + for (const pin of response.data.items) { + if (pin.message) { + const user = this.rocket.findUser(pin.message.user); + const msgObj = { + rid: rid, + t: 'message_pinned', + msg: '', + u: { + _id: user._id, + username: user.username + }, + attachments: [{ + 'text' : this.rocket.convertSlackMsgTxtToRocketTxtFormat(pin.message.text), + 'author_name' : user.username, + 'author_icon' : getAvatarUrlFromUsername(user.username), + 'ts' : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000) + }] + }; + + RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000)); + } + } + } + } + + importMessages(rid, callback) { + logger.slack.info('importMessages: ', rid); + const rocketchat_room = RocketChat.models.Rooms.findOneById(rid); + if (rocketchat_room) { + if (this.getSlackChannel(rid)) { + this.copyChannelInfo(rid, this.getSlackChannel(rid)); + + logger.slack.debug('Importing messages from Slack to Rocket.Chat', this.getSlackChannel(rid), rid); + let results = this.importFromHistory(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: 1 }); + while (results && results.has_more) { + results = this.importFromHistory(this.getSlackChannel(rid).family, { channel: this.getSlackChannel(rid).id, oldest: results.ts }); + } + + logger.slack.debug('Pinning Slack channel messages to Rocket.Chat', this.getSlackChannel(rid), rid); + this.copyPins(rid, this.getSlackChannel(rid)); + + return callback(); + } else { + const slack_room = this.postFindChannel(rocketchat_room.name); + if (slack_room) { + this.addSlackChannel(rid, slack_room.id); + return this.importMessages(rid, callback); + } else { + logger.slack.error('Could not find Slack room with specified name', rocketchat_room.name); + return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name')); + } + } + } else { + logger.slack.error('Could not find Rocket.Chat room with specified id', rid); + return callback(new Meteor.Error('error-invalid-room', 'Invalid room')); + } + } + +} + +SB_SlackAdapter = SlackAdapter; + diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index 4881fee53bd7..616e1760a33e 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -1,1365 +1,96 @@ -/* globals logger */ +/* globals logger SB_SlackAdapter SB_RocketAdapter*/ +/** + * SlackBridge interfaces between this Rocket installation and a remote Slack installation. + */ class SlackBridge { constructor() { - this.util = Npm.require('util'); - this.slackClient = Npm.require('slack-client'); - this.apiToken = RocketChat.settings.get('SlackBridge_APIToken'); - this.aliasFormat = RocketChat.settings.get('SlackBridge_AliasFormat'); - this.excludeBotnames = RocketChat.settings.get('SlackBridge_Botnames'); - this.rtm = {}; + this.slack = new SB_SlackAdapter(this); + this.rocket = new SB_RocketAdapter(this); + this.reactionsMap = new Map(); //Sync object between rocket and slack this.connected = false; - this.userTags = {}; - this.slackChannelMap = {}; - this.reactionsMap = new Map(); + this.rocket.setSlack(this.slack); + this.slack.setRocket(this.rocket); - RocketChat.settings.get('SlackBridge_APIToken', (key, value) => { - if (value !== this.apiToken) { - this.apiToken = value; - if (this.connected) { - this.disconnect(); - this.connect(); - } - } - }); - - RocketChat.settings.get('SlackBridge_AliasFormat', (key, value) => { - this.aliasFormat = value; - }); - - RocketChat.settings.get('SlackBridge_ExcludeBotnames', (key, value) => { - this.excludeBotnames = value; - }); - - RocketChat.settings.get('SlackBridge_Enabled', (key, value) => { - if (value && this.apiToken) { - this.connect(); - } else { - this.disconnect(); - } - }); + this.processSettings(); } connect() { if (this.connected === false) { + + this.slack.connect(this.apiToken); + if (RocketChat.settings.get('SlackBridge_Out_Enabled')) { + this.rocket.connect(); + } + this.connected = true; - logger.connection.info('Connecting via token: ', this.apiToken); - var RtmClient = this.slackClient.RtmClient; - this.rtm = new RtmClient(this.apiToken); - this.rtm.start(); - this.registerForSlackEvents(); - RocketChat.settings.get('SlackBridge_Out_Enabled', (key, value) => { - if (value) { - this.registerForRocketEvents(); - } else { - this.unregisterForRocketEvents(); - } - }); - Meteor.startup(() => { - try { - this.populateSlackChannelMap(); // If run outside of Meteor.startup, HTTP is not defined - } catch (err) { - logger.class.error('Error attempting to connect to Slack', err); - this.disconnect(); - } - }); + logger.connection.info('Enabled'); } } disconnect() { if (this.connected === true) { + this.rocket.disconnect(); + this.slack.disconnect(); this.connected = false; - this.rtm.disconnect && this.rtm.disconnect(); - logger.connection.info('Disconnected'); - this.unregisterForRocketEvents(); - } - } - - convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) { - if (!_.isEmpty(slackMsgTxt)) { - slackMsgTxt = slackMsgTxt.replace(//g, '@all'); - slackMsgTxt = slackMsgTxt.replace(//g, '@all'); - slackMsgTxt = slackMsgTxt.replace(/>/g, '<'); - slackMsgTxt = slackMsgTxt.replace(/</g, '>'); - slackMsgTxt = slackMsgTxt.replace(/&/g, '&'); - slackMsgTxt = slackMsgTxt.replace(/:simple_smile:/g, ':smile:'); - slackMsgTxt = slackMsgTxt.replace(/:memo:/g, ':pencil:'); - slackMsgTxt = slackMsgTxt.replace(/:piggy:/g, ':pig:'); - slackMsgTxt = slackMsgTxt.replace(/:uk:/g, ':gb:'); - slackMsgTxt = slackMsgTxt.replace(/<(http[s]?:[^>]*)>/g, '$1'); - - slackMsgTxt.replace(/(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g, (match, userId) => { - if (!this.userTags[userId]) { - this.findRocketUser(userId) || this.addRocketUser(userId); // This adds userTags for the userId - } - const userTags = this.userTags[userId]; - if (userTags) { - slackMsgTxt = slackMsgTxt.replace(userTags.slack, userTags.rocket); - } - }); - } else { - slackMsgTxt = ''; - } - return slackMsgTxt; - } - - findRocketChannel(slackChannelId) { - return RocketChat.models.Rooms.findOneByImportId(slackChannelId); - } - - addRocketChannel(slackChannelID, hasRetried = false) { - logger.class.debug('Adding Rocket.Chat channel from Slack', slackChannelID); - let slackResults = null; - let isGroup = false; - if (slackChannelID.charAt(0) === 'C') { - slackResults = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.apiToken, channel: slackChannelID } }); - } else if (slackChannelID.charAt(0) === 'G') { - slackResults = HTTP.get('https://slack.com/api/groups.info', { params: { token: this.apiToken, channel: slackChannelID } }); - isGroup = true; - } - if (slackResults && slackResults.data && slackResults.data.ok === true) { - const rocketChannelData = isGroup ? slackResults.data.group : slackResults.data.channel; - const existingRocketRoom = RocketChat.models.Rooms.findOneByName(rocketChannelData.name); - - // If the room exists, make sure we have its id in importIds - if (existingRocketRoom || rocketChannelData.is_general) { - rocketChannelData.rocketId = rocketChannelData.is_general ? 'GENERAL' : existingRocketRoom._id; - RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); - } else { - const rocketUsers = []; - for (const member of rocketChannelData.members) { - if (member !== rocketChannelData.creator) { - const rocketUser = this.findRocketUser(member) || this.addRocketUser(member); - if (rocketUser && rocketUser.username) { - rocketUsers.push(rocketUser.username); - } - } - } - const rocketUserCreator = rocketChannelData.creator ? this.findRocketUser(rocketChannelData.creator) || this.addRocketUser(rocketChannelData.creator) : null; - if (!rocketUserCreator) { - logger.class.error('Could not fetch room creator information', rocketChannelData.creator); - return; - } - - try { - const rocketChannel = RocketChat.createRoom(isGroup ? 'p' : 'c', rocketChannelData.name, rocketUserCreator.username, rocketUsers); - rocketChannelData.rocketId = rocketChannel.rid; - } catch (e) { - if (!hasRetried) { - logger.class.debug('Error adding channel from Slack. Will retry in 1s.', e.message); - // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. - Meteor._sleepForMs(1000); - return this.findRocketChannel(slackChannelID) || this.addRocketChannel(slackChannelID, true); - } else { - console.log(e.message); - } - } - - const roomUpdate = { - ts: new Date(rocketChannelData.created * 1000) - }; - let lastSetTopic = 0; - if (!_.isEmpty(rocketChannelData.topic && rocketChannelData.topic.value)) { - roomUpdate.topic = rocketChannelData.topic.value; - lastSetTopic = rocketChannelData.topic.last_set; - } - if (!_.isEmpty(rocketChannelData.purpose && rocketChannelData.purpose.value) && rocketChannelData.purpose.last_set > lastSetTopic) { - roomUpdate.topic = rocketChannelData.purpose.value; - } - RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); - this.slackChannelMap[rocketChannelData.rocketId] = { id: slackChannelID, family: slackChannelID.charAt(0) === 'C' ? 'channels' : 'groups' }; - } - return RocketChat.models.Rooms.findOneById(rocketChannelData.rocketId); - } - logger.class.debug('Channel not added'); - return; - } - - findRocketUser(slackUserID) { - const rocketUser = RocketChat.models.Users.findOneByImportId(slackUserID); - if (rocketUser && !this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUser.username}` }; - } - return rocketUser; - } - - addRocketUser(slackUserID) { - logger.class.debug('Adding Rocket.Chat user from Slack', slackUserID); - const slackResults = HTTP.get('https://slack.com/api/users.info', { params: { token: this.apiToken, user: slackUserID } }); - if (slackResults && slackResults.data && slackResults.data.ok === true && slackResults.data.user) { - const rocketUserData = slackResults.data.user; - const isBot = rocketUserData.is_bot === true; - const email = rocketUserData.profile && rocketUserData.profile.email || ''; - let existingRocketUser; - if (!isBot) { - existingRocketUser = RocketChat.models.Users.findOneByEmailAddress(email) || RocketChat.models.Users.findOneByUsername(rocketUserData.name); - } else { - existingRocketUser = RocketChat.models.Users.findOneByUsername(rocketUserData.name); - } - - if (existingRocketUser) { - rocketUserData.rocketId = existingRocketUser._id; - rocketUserData.name = existingRocketUser.username; - } else { - const newUser = { - password: Random.id(), - username: rocketUserData.name - }; - - if (!isBot && email) { - newUser.email = email; - } - - if (isBot) { - newUser.joinDefaultChannels = false; - } - - rocketUserData.rocketId = Accounts.createUser(newUser); - const userUpdate = { - utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, - roles: isBot ? [ 'bot' ] : [ 'user' ] - }; - - if (rocketUserData.profile && rocketUserData.profile.real_name) { - userUpdate['name'] = rocketUserData.profile.real_name; - } - - if (rocketUserData.deleted) { - userUpdate['active'] = false; - userUpdate['services.resume.loginTokens'] = []; - } - - RocketChat.models.Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate }); - - const user = RocketChat.models.Users.findOneById(rocketUserData.rocketId); - - let url = null; - if (rocketUserData.profile) { - if (rocketUserData.profile.image_original) { - url = rocketUserData.profile.image_original; - } else if (rocketUserData.profile.image_512) { - url = rocketUserData.profile.image_512; - } - } - if (url) { - try { - RocketChat.setUserAvatar(user, url, null, 'url'); - } catch (error) { - logger.class.debug('Error setting user avatar', error.message); - } - } - } - - const importIds = [ rocketUserData.id ]; - if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) { - importIds.push(rocketUserData.profile.bot_id); - } - RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); - if (!this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUserData.name}` }; - } - return RocketChat.models.Users.findOneById(rocketUserData.rocketId); - } - logger.class.debug('User not added'); - return; - } - - addAliasToRocketMsg(rocketUserName, rocketMsgObj) { - if (this.aliasFormat) { - var alias = this.util.format(this.aliasFormat, rocketUserName); - - if (alias !== rocketUserName) { - rocketMsgObj.alias = alias; - } - } - - return rocketMsgObj; - } - - createAndSaveRocketMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting) { - if (slackMessage.type === 'message') { - let rocketMsgObj = {}; - if (!_.isEmpty(slackMessage.subtype)) { - rocketMsgObj = this.processSlackSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting); - if (!rocketMsgObj) { - return; - } - } else { - rocketMsgObj = { - msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), - rid: rocketChannel._id, - u: { - _id: rocketUser._id, - username: rocketUser.username - } - }; - - this.addAliasToRocketMsg(rocketUser.username, rocketMsgObj); - } - _.extend(rocketMsgObj, rocketMsgDataDefaults); - if (slackMessage.edited) { - rocketMsgObj.editedAt = new Date(parseInt(slackMessage.edited.ts.split('.')[0]) * 1000); - } - if (slackMessage.subtype === 'bot_message') { - rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); - } - - if (slackMessage.pinned_to && slackMessage.pinned_to.indexOf(slackMessage.channel) !== -1) { - rocketMsgObj.pinned = true; - rocketMsgObj.pinnedAt = Date.now; - rocketMsgObj.pinnedBy = _.pick(rocketUser, '_id', 'username'); - } - if (slackMessage.subtype === 'bot_message') { - Meteor.setTimeout(() => { - if (slackMessage.bot_id && slackMessage.ts && !RocketChat.models.Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) { - RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); - } - }, 500); - } else { - logger.class.debug('Send message to Rocket.Chat'); - RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); - } - } - } - - /* - https://api.slack.com/events/reaction_removed - */ - onSlackReactionRemoved(slackReactionMsg) { - if (slackReactionMsg) { - const rocketUser = this.getRocketUser(slackReactionMsg.user); - //Lets find our Rocket originated message - let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); - - if (!rocketMsg) { - //Must have originated from Slack - const rocketID = this.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); - rocketMsg = RocketChat.models.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.reactionsMap.set('unset'+rocketMsg._id+rocketReaction, rocketUser); - logger.class.debug('Removing reaction from Slack'); - Meteor.runAsUser(rocketUser._id, () => { - Meteor.call('setReaction', rocketReaction, rocketMsg._id); - }); - } + logger.connection.info('Disabled'); } } - /* - https://api.slack.com/events/reaction_added - */ - onSlackReactionAdded(slackReactionMsg) { - if (slackReactionMsg) { - const rocketUser = this.getRocketUser(slackReactionMsg.user); - - if (rocketUser.roles.includes('bot')) { - return; - } - - //Lets find our Rocket originated message - let rocketMsg = RocketChat.models.Messages.findOneBySlackTs(slackReactionMsg.item.ts); - - if (!rocketMsg) { - //Must have originated from Slack - const rocketID = this.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts); - rocketMsg = RocketChat.models.Messages.findOneById(rocketID); - } - - if (rocketMsg && rocketUser) { - const rocketReaction = ':' + slackReactionMsg.reaction + ':'; + processSettings() { - //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.reactionsMap.set('set'+rocketMsg._id+rocketReaction, rocketUser); - logger.class.debug('Adding reaction from Slack'); - Meteor.runAsUser(rocketUser._id, () => { - Meteor.call('setReaction', rocketReaction, rocketMsg._id); - }); - } - } - } - - /** - * We have received a message from slack and we need to save/delete/update it into rocket - * https://api.slack.com/events/message - */ - onSlackMessage(slackMessage, isImporting) { - if (slackMessage.subtype) { - switch (slackMessage.subtype) { - case 'message_deleted': - this.processSlackMessageDeleted(slackMessage); - break; - case 'message_changed': - this.processSlackMessageChanged(slackMessage); - break; - default: - //Keeping backwards compatability for now, refactor later - this.processSlackNewMessage(slackMessage, isImporting); - } - } else { - //Simple message - this.processSlackNewMessage(slackMessage, isImporting); - } - } - - processSlackSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) { - let rocketMsgObj = null; - switch (slackMessage.subtype) { - case 'bot_message': - if (slackMessage.username !== undefined && this.excludeBotnames && slackMessage.username.match(this.excludeBotnames)) { - return; - } - - rocketMsgObj = { - msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), - rid: rocketChannel._id, - bot: true, - attachments: slackMessage.attachments, - username: slackMessage.username || slackMessage.bot_id - }; - this.addAliasToRocketMsg(slackMessage.username || slackMessage.bot_id, rocketMsgObj); - if (slackMessage.icons) { - rocketMsgObj.emoji = slackMessage.icons.emoji; - } - return rocketMsgObj; - case 'me_message': - return this.addAliasToRocketMsg(rocketUser.username, { - msg: `_${this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text)}_` - }); - case 'channel_join': - if (isImporting) { - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.addUserToRoom(rocketChannel._id, rocketUser); - } - return; - case 'group_join': - if (slackMessage.inviter) { - const inviter = slackMessage.inviter ? this.findRocketUser(slackMessage.inviter) || this.addRocketUser(slackMessage.inviter) : null; - if (isImporting) { - RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, { - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), - u: { - _id: inviter._id, - username: inviter.username - }, - imported: 'slackbridge' - }); - } else { - RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter); - } - } - return; - case 'channel_leave': - case 'group_leave': - if (isImporting) { - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, { - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), - imported: 'slackbridge' - }); - } else { - RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser); - } - return; - case 'channel_topic': - case 'group_topic': - if (isImporting) { - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false); - } - return; - case 'channel_purpose': - case 'group_purpose': - if (isImporting) { - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false); - } - return; - case 'channel_name': - case 'group_name': - if (isImporting) { - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false); - } - return; - case 'channel_archive': - case 'group_archive': - if (!isImporting) { - RocketChat.archiveRoom(rocketChannel); - } - return; - case 'channel_unarchive': - case 'group_unarchive': - if (!isImporting) { - RocketChat.unarchiveRoom(rocketChannel); - } - return; - case 'file_share': - 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); - } - break; - case 'file_comment': - logger.class.error('File comment not implemented'); - return; - case 'file_mention': - logger.class.error('File mentioned not implemented'); - return; - case 'pinned_item': - if (slackMessage.attachments && slackMessage.attachments[0] && slackMessage.attachments[0].text) { - rocketMsgObj = { - rid: rocketChannel._id, - t: 'message_pinned', - msg: '', - u: { - _id: rocketUser._id, - username: rocketUser.username - }, - attachments: [{ - 'text' : this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text), - 'author_name' : slackMessage.attachments[0].author_subname, - 'author_icon' : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname), - 'ts' : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000) - }] - }; - - if (!isImporting) { - RocketChat.models.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; - } else { - logger.class.error('Pinned item with no attachment'); + //Slack installation API token + RocketChat.settings.get('SlackBridge_APIToken', (key, value) => { + if (value !== this.apiToken) { + this.apiToken = value; + if (this.connected) { + this.disconnect(); + this.connect(); } - return; - case 'unpinned_item': - logger.class.error('Unpinned item not implemented'); - return; - } - } - - /** - 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); - uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) { - const url = Npm.require('url'); - const requestModule = /https/i.test(slackFileURL) ? Npm.require('https') : Npm.require('http'); - var parsedUrl = url.parse(slackFileURL, true); - parsedUrl.headers = { 'Authorization': 'Bearer ' + this.apiToken }; - requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => { - const fileId = Meteor.fileStore.create(details); - if (fileId) { - Meteor.fileStore.write(stream, fileId, (err, file) => { - console.log('fileStore.write', file); - if (err) { - throw new Error(err); - } else { - const url = file.url.replace(Meteor.absoluteUrl(), '/'); - const attachment = { - title: `File Uploaded: ${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; - } - - return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); - } - }); } - })); - } - - registerForRocketEvents() { - RocketChat.callbacks.add('afterSaveMessage', this.onRocketMessage.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Out'); - RocketChat.callbacks.add('afterDeleteMessage', this.onRocketMessageDelete.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Delete'); - RocketChat.callbacks.add('setReaction', this.onRocketSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_SetReaction'); - RocketChat.callbacks.add('unsetReaction', this.onRocketUnSetReaction.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); - } - - unregisterForRocketEvents() { - RocketChat.callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); - RocketChat.callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); - RocketChat.callbacks.remove('setReaction', 'SlackBridge_SetReaction'); - RocketChat.callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction'); - } - registerForSlackEvents() { - var CLIENT_EVENTS = this.slackClient.CLIENT_EVENTS; - this.rtm.on(CLIENT_EVENTS.RTM.AUTHENTICATED, () => { - logger.connection.info('Connected to Slack'); + logger.class.debug('Setting: ' + key, value); }); - this.rtm.on(CLIENT_EVENTS.RTM.UNABLE_TO_RTM_START, () => { - this.disconnect(); + //Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used. + RocketChat.settings.get('SlackBridge_AliasFormat', (key, value) => { + this.aliasFormat = value; + logger.class.debug('Setting: ' + key, value); }); - this.rtm.on(CLIENT_EVENTS.RTM.DISCONNECT, () => { - this.disconnect(); + //Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated. + RocketChat.settings.get('SlackBridge_ExcludeBotnames', (key, value) => { + this.excludeBotnames = value; + logger.class.debug('Setting: ' + key, value); }); - var RTM_EVENTS = this.slackClient.RTM_EVENTS; - - /** - * 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(RTM_EVENTS.MESSAGE, Meteor.bindEnvironment((slackMessage) => { - logger.events.debug('OnSlackEvent-MESSAGE: ', slackMessage); - if (slackMessage) { - this.onSlackMessage(slackMessage); - } - })); - - this.rtm.on(RTM_EVENTS.REACTION_ADDED, Meteor.bindEnvironment((reactionMsg) => { - logger.events.debug('OnSlackEvent-REACTION_ADDED: ', reactionMsg); - if (reactionMsg) { - this.onSlackReactionAdded(reactionMsg); - } - })); - - this.rtm.on(RTM_EVENTS.REACTION_REMOVED, Meteor.bindEnvironment((reactionMsg) => { - logger.events.debug('OnSlackEvent-REACTION_REMOVED: ', reactionMsg); - if (reactionMsg) { - this.onSlackReactionRemoved(reactionMsg); - } - })); - - /** - * 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(RTM_EVENTS.CHANNEL_CREATED, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.CHANNEL_JOINED, Meteor.bindEnvironment(() => {})); - - /** - * Event fired when the bot leaves (or is removed from) a public channel - * { - * type: 'channel_left', - * channel: [channel_id] - * } - **/ - this.rtm.on(RTM_EVENTS.CHANNEL_LEFT, Meteor.bindEnvironment(() => {})); - - /** - * Event fired when an archived channel is deleted by an admin - * { - * type: 'channel_deleted', - * channel: [channel_id], - * event_ts: [ts.milli] - * } - **/ - this.rtm.on(RTM_EVENTS.CHANNEL_DELETED, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.CHANNEL_RENAME, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.GROUP_JOINED, Meteor.bindEnvironment(() => {})); - - /** - * Event fired when the bot leaves (or is removed from) a private channel - * { - * type: 'group_left', - * channel: [channel_id] - * } - **/ - this.rtm.on(RTM_EVENTS.GROUP_LEFT, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.GROUP_RENAME, Meteor.bindEnvironment(() => {})); - - /** - * 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(RTM_EVENTS.TEAM_JOIN, Meteor.bindEnvironment(() => {})); - } - - findSlackChannel(rocketChannelName) { - logger.class.debug('Searching for Slack channel or group', rocketChannelName); - let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { - for (const channel of response.data.channels) { - if (channel.name === rocketChannelName && channel.is_member === true) { - return channel; - } - } - } - response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { - for (const group of response.data.groups) { - if (group.name === rocketChannelName) { - return group; - } - } - } - } - - importFromHistory(family, options) { - logger.class.debug('Importing messages history'); - const response = HTTP.get('https://slack.com/api/' + family + '.history', { params: _.extend({ token: this.apiToken }, options) }); - if (response && response.data && _.isArray(response.data.messages) && response.data.messages.length > 0) { - let latest = 0; - for (const message of response.data.messages.reverse()) { - logger.class.debug('MESSAGE: ', message); - if (!latest || message.ts > latest) { - latest = message.ts; - } - message.channel = options.channel; - this.onSlackMessage(message, true); - } - return { has_more: response.data.has_more, ts: latest }; - } - } - - copySlackChannelInfo(rid, channelMap) { - logger.class.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid); - const response = HTTP.get('https://slack.com/api/' + channelMap.family + '.info', { params: { token: this.apiToken, channel: channelMap.id } }); - if (response && response.data) { - const data = channelMap.family === 'channels' ? response.data.channel : response.data.group; - if (data && _.isArray(data.members) && data.members.length > 0) { - for (const member of data.members) { - const user = this.findRocketUser(member) || this.addRocketUser(member); - if (user) { - logger.class.debug('Adding user to room', user.username, rid); - RocketChat.addUserToRoom(rid, user, null, true); - } - } - } - - let topic = ''; - let topic_last_set = 0; - let topic_creator = null; - if (data && data.topic && data.topic.value) { - topic = data.topic.value; - topic_last_set = data.topic.last_set; - topic_creator = data.topic.creator; - } - - if (data && data.purpose && data.purpose.value) { - if (topic_last_set) { - if (topic_last_set < data.purpose.last_set) { - topic = data.purpose.topic; - topic_creator = data.purpose.creator; - } - } else { - topic = data.purpose.topic; - topic_creator = data.purpose.creator; - } - } - - if (topic) { - const creator = this.findRocketUser(topic_creator) || this.addRocketUser(topic_creator); - logger.class.debug('Setting room topic', rid, topic, creator.username); - RocketChat.saveRoomTopic(rid, topic, creator, false); - } - } - } - - copyPins(rid, channelMap) { - const response = HTTP.get('https://slack.com/api/pins.list', { params: { token: this.apiToken, channel: channelMap.id } }); - if (response && response.data && _.isArray(response.data.items) && response.data.items.length > 0) { - for (const pin of response.data.items) { - if (pin.message) { - const user = this.findRocketUser(pin.message.user); - const msgObj = { - rid: rid, - t: 'message_pinned', - msg: '', - u: { - _id: user._id, - username: user.username - }, - attachments: [{ - 'text' : this.convertSlackMsgTxtToRocketTxtFormat(pin.message.text), - 'author_name' : user.username, - 'author_icon' : getAvatarUrlFromUsername(user.username), - 'ts' : new Date(parseInt(pin.message.ts.split('.')[0]) * 1000) - }] - }; - - RocketChat.models.Messages.setPinnedByIdAndUserId(`slack-${pin.channel}-${pin.message.ts.replace(/\./g, '-')}`, msgObj.u, true, new Date(parseInt(pin.message.ts.split('.')[0]) * 1000)); - } - } - } - } + //Choose whether SlackBridge should also send your messages back to Slack + RocketChat.settings.get('SlackBridge_Out_Enabled', (key, value) => { + logger.class.debug('Setting: ' + key, value); + }); - importMessages(rid, callback) { - logger.class.info('importMessages: ', rid); - const rocketchat_room = RocketChat.models.Rooms.findOneById(rid); - if (rocketchat_room) { - if (this.slackChannelMap[rid]) { - this.copySlackChannelInfo(rid, this.slackChannelMap[rid]); + //Send messages from all channels that exist in Slack and the bot has joined + RocketChat.settings.get('SlackBridge_Out_All', (key, value) => { + logger.class.debug('Setting: ' + key, value); + }); - logger.class.debug('Importing messages from Slack to Rocket.Chat', this.slackChannelMap[rid], rid); - let results = this.importFromHistory(this.slackChannelMap[rid].family, { channel: this.slackChannelMap[rid].id, oldest: 1 }); - while (results && results.has_more) { - results = this.importFromHistory(this.slackChannelMap[rid].family, { channel: this.slackChannelMap[rid].id, oldest: results.ts }); - } + //Choose which channels will send messages back to Slack + RocketChat.settings.get('SlackBridge_Out_Channels', (key, value) => { + logger.class.debug('Setting: ' + key, value); + }); - logger.class.debug('Pinning Slack channel messages to Rocket.Chat', this.slackChannelMap[rid], rid); - this.copyPins(rid, this.slackChannelMap[rid]); - return callback(); + //Is this entire SlackBridge enabled + RocketChat.settings.get('SlackBridge_Enabled', (key, value) => { + if (value && this.apiToken) { + this.connect(); } else { - const slack_room = this.findSlackChannel(rocketchat_room.name); - if (slack_room) { - this.slackChannelMap[rid] = { id: slack_room.id, family: slack_room.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - return this.importMessages(rid, callback); - } else { - logger.class.error('Could not find Slack room with specified name', rocketchat_room.name); - return callback(new Meteor.Error('error-slack-room-not-found', 'Could not find Slack room with specified name')); - } - } - } else { - logger.class.error('Could not find Rocket.Chat room with specified id', rid); - return callback(new Meteor.Error('error-invalid-room', 'Invalid room')); - } - } - - populateSlackChannelMap() { - logger.class.debug('Populating channel map'); - let response = HTTP.get('https://slack.com/api/channels.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.channels) && response.data.channels.length > 0) { - for (const slackChannel of response.data.channels) { - const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } }); - if (rocketchat_room) { - this.slackChannelMap[rocketchat_room._id] = { id: slackChannel.id, family: slackChannel.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - } - } - } - response = HTTP.get('https://slack.com/api/groups.list', { params: { token: this.apiToken } }); - if (response && response.data && _.isArray(response.data.groups) && response.data.groups.length > 0) { - for (const slackGroup of response.data.groups) { - const rocketchat_room = RocketChat.models.Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } }); - if (rocketchat_room) { - this.slackChannelMap[rocketchat_room._id] = { id: slackGroup.id, family: slackGroup.id.charAt(0) === 'C' ? 'channels' : 'groups' }; - } - } - } - } - - onRocketMessageDelete(rocketMessageDeleted) { - logger.class.debug('onRocketMessageDelete', rocketMessageDeleted); - - this.postDeleteMessageToSlack(rocketMessageDeleted); - } - - onRocketSetReaction(rocketMsgID, reaction) { - logger.class.debug('onRocketSetReaction'); - - if (rocketMsgID && reaction) { - if (this.reactionsMap.delete('set'+rocketMsgID+reaction)) { - //This was a Slack reaction, we don't need to tell Slack about it - return; - } - const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); - if (rocketMsg) { - const slackChannel = this.slackChannelMap[rocketMsg.rid].id; - const slackTS = this.getSlackTS(rocketMsg); - this.postReactionAddedToSlack(reaction.replace(/:/g, ''), slackChannel, slackTS); - } - } - } - - onRocketUnSetReaction(rocketMsgID, reaction) { - logger.class.debug('onRocketUnSetReaction'); - - if (rocketMsgID && reaction) { - if (this.reactionsMap.delete('unset'+rocketMsgID+reaction)) { - //This was a Slack unset reaction, we don't need to tell Slack about it - return; - } - - const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); - if (rocketMsg) { - const slackChannel = this.slackChannelMap[rocketMsg.rid].id; - const slackTS = this.getSlackTS(rocketMsg); - this.postReactionRemoveToSlack(reaction.replace(/:/g, ''), slackChannel, slackTS); - } - } - } - - onRocketMessage(rocketMessage) { - logger.class.debug('onRocketMessage', rocketMessage); - - if (rocketMessage.editedAt) { - //This is an Edit Event - this.processRocketMessageChanged(rocketMessage); - return rocketMessage; - } - // Ignore messages originating from Slack - if (rocketMessage._id.indexOf('slack-') === 0) { - return rocketMessage; - } - - //Probably a new message from Rocket.Chat - const outSlackChannels = RocketChat.settings.get('SlackBridge_Out_All') ? _.keys(this.slackChannelMap) : _.pluck(RocketChat.settings.get('SlackBridge_Out_Channels'), '_id') || []; - //logger.class.debug('Out SlackChannels: ', outSlackChannels); - if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) { - this.postMessageToSlack(this.slackChannelMap[rocketMessage.rid], rocketMessage); - } - return rocketMessage; - } - - /* - https://api.slack.com/methods/reactions.add - */ - postReactionAddedToSlack(reaction, slackChannel, slackTS) { - if (reaction && slackChannel && slackTS) { - const data = { - token: this.apiToken, - name: reaction, - channel: slackChannel, - timestamp: slackTS - }; - - logger.class.debug('Posting Add Reaction to Slack'); - const postResult = HTTP.post('https://slack.com/api/reactions.add', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Reaction added to Slack'); - } - } - } - - /* - https://api.slack.com/methods/reactions.remove - */ - postReactionRemoveToSlack(reaction, slackChannel, slackTS) { - if (reaction && slackChannel && slackTS) { - const data = { - token: this.apiToken, - name: reaction, - channel: slackChannel, - timestamp: slackTS - }; - - logger.class.debug('Posting Remove Reaction to Slack'); - const postResult = HTTP.post('https://slack.com/api/reactions.remove', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Reaction removed from Slack'); - } - } - } - - postDeleteMessageToSlack(rocketMessage) { - if (rocketMessage) { - const data = { - token: this.apiToken, - ts: this.getSlackTS(rocketMessage), - channel: this.slackChannelMap[rocketMessage.rid].id, - as_user: true - }; - - logger.class.debug('Post Delete Message to Slack', data); - const postResult = HTTP.post('https://slack.com/api/chat.delete', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Message deleted on Slack'); - } - } - } - - postMessageToSlack(slackChannel, rocketMessage) { - if (slackChannel && slackChannel.id) { - let iconUrl = getAvatarUrlFromUsername(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 - }; - logger.class.debug('Post Message To Slack', data); - const postResult = HTTP.post('https://slack.com/api/chat.postMessage', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { - RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); - logger.class.debug('RocketMsgID=' + rocketMessage._id + ' SlackMsgID=' + postResult.data.message.ts + ' SlackBotID=' + postResult.data.message.bot_id); - } - } - } - - /* - https://api.slack.com/methods/chat.update - */ - postMessageUpdateToSlack(slackChannel, rocketMessage) { - if (slackChannel && slackChannel.id) { - const data = { - token: this.apiToken, - ts: this.getSlackTS(rocketMessage), - channel: slackChannel.id, - text: rocketMessage.msg, - as_user: true - }; - logger.class.debug('Post UpdateMessage To Slack', data); - const postResult = HTTP.post('https://slack.com/api/chat.update', { params: data }); - if (postResult.statusCode === 200 && postResult.data && postResult.data.ok === true) { - logger.class.debug('Message updated on Slack'); - } - } - } - - processRocketMessageChanged(rocketMessage) { - if (rocketMessage) { - if (rocketMessage.updatedBySlack) { - //We have already processed this - delete rocketMessage.updatedBySlack; - return; - } - - //This was a change from Rocket.Chat - const slackChannel = this.slackChannelMap[rocketMessage.rid]; - this.postMessageUpdateToSlack(slackChannel, rocketMessage); - } - } - - /* - https://api.slack.com/events/message/message_deleted - */ - processSlackMessageDeleted(slackMessage) { - if (slackMessage.previous_message) { - const rocketChannel = this.getRocketChannel(slackMessage); - const rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); - - if (rocketChannel && rocketUser) { - //Find the Rocket message to delete - let rocketMsgObj = RocketChat.models.Messages - .findOneBySlackBotIdAndSlackTs(slackMessage.previous_message.bot_id, slackMessage.previous_message.ts); - - if (!rocketMsgObj) { - //Must have been a Slack originated msg - const _id = this.createRocketID(slackMessage.channel, slackMessage.previous_message.ts); - rocketMsgObj = RocketChat.models.Messages.findOneById(_id); - } - - if (rocketMsgObj) { - RocketChat.deleteMessage(rocketMsgObj, rocketUser); - logger.class.debug('Rocket message deleted by Slack'); - } - } - } - } - - /* - https://api.slack.com/events/message/message_changed - */ - processSlackMessageChanged(slackMessage) { - if (slackMessage.previous_message) { - const currentMsg = RocketChat.models.Messages.findOneById(this.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 = this.getRocketChannel(slackMessage); - const rocketUser = slackMessage.previous_message.user ? this.findRocketUser(slackMessage.previous_message.user) || this.addRocketUser(slackMessage.previous_message.user) : null; - - const rocketMsgObj = { - //@TODO _id - _id: this.createRocketID(slackMessage.channel, slackMessage.previous_message.ts), - rid: rocketChannel._id, - msg: this.convertSlackMsgTxtToRocketTxtFormat(slackMessage.message.text), - updatedBySlack: true //We don't want to notify slack about this change since Slack initiated it - }; - - RocketChat.updateMessage(rocketMsgObj, rocketUser); - logger.class.debug('Rocket message updated by Slack'); - } - } - } - - /* - This method will get refactored and broken down into single responsibilities - */ - processSlackNewMessage(slackMessage, isImporting) { - const rocketChannel = this.getRocketChannel(slackMessage); - let rocketUser = null; - if (slackMessage.subtype === 'bot_message') { - rocketUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 } }); - } else { - rocketUser = slackMessage.user ? this.findRocketUser(slackMessage.user) || this.addRocketUser(slackMessage.user) : null; - } - if (rocketChannel && rocketUser) { - const msgDataDefaults = { - _id: this.createRocketID(slackMessage.channel, slackMessage.ts), - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000) - }; - if (isImporting) { - msgDataDefaults['imported'] = 'slackbridge'; - } - try { - this.createAndSaveRocketMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting); - } catch (e) { - // http://www.mongodb.org/about/contributors/error-codes/ - // 11000 == duplicate key error - if (e.name === 'MongoError' && e.code === 11000) { - return; - } - - throw e; + this.disconnect(); } - } - } - - /** - * 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 - */ - getSlackTS(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; - } - - getRocketChannel(slackMessage) { - return slackMessage.channel ? this.findRocketChannel(slackMessage.channel) || this.addRocketChannel(slackMessage.channel) : null; - } - - getRocketUser(slackUser) { - return slackUser ? this.findRocketUser(slackUser) || this.addRocketUser(slackUser) : null; - } - - createRocketID(slackChannel, ts) { - return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`; + logger.class.debug('Setting: ' + key, value); + }); } - } RocketChat.SlackBridge = new SlackBridge; diff --git a/packages/rocketchat-slackbridge/tests/manual-tests.txt b/packages/rocketchat-slackbridge/tests/manual-tests.txt new file mode 100644 index 000000000000..a978998a57d6 --- /dev/null +++ b/packages/rocketchat-slackbridge/tests/manual-tests.txt @@ -0,0 +1,58 @@ +Config: + + + +Sending Message +* Send slack msg to rocket +* Send rocket msg to slack +* Repeat on a channel that rocketbot isn't a member + + +Edit Message: +* Edit slack msg from slack +* Edit rocket msg from rocket +* Edit slack msg from rocket - Expected fail, slack not updated (slack says can't update message) +* Edit rocket msg from slack - Expected fail, can't even edit +* Repeat on a channel that rocketbot isn't a member + + +Delete Message: +* Delete slack message in slack +* Delete rocket message in rocket +* Delete rocket message in slack +* Delete slack message in rocket - Expected fail, slack not updated, deleted in rocket +* Repeat on a channel that rocketbot isn't a member + + +React to a Message: +* React to a slack message in slack +* React to a rocket message in slack +* React to a rocket message in rocket +* React to a slack message in rocket +* Repeat on a channel that rocketbot isn't a member + + +Channels: +Channel exists on both, but no rocketbot. +* Add rocket bot to slack, send messages from both + +Channel exists on rocket only +* Create slack channel, add rocket bot: +** Send message from rocket +** Send message from slack + +Channel exists on slack only +* Add rocket bot to slack: +** Send message from slack +** Send message from rocket + +Channel exists on both w/ rocketbot +* Remove rocket channel, send messages from slack (same as ch exists on slack only) +* Remove rocketbot from slack: +** Send message from slack +** Send message from rocket + + + + + From 6b237ee9b4dd3febc94eda668bd08ec9d44723e8 Mon Sep 17 00:00:00 2001 From: kablewi Date: Thu, 9 Mar 2017 10:14:45 -0800 Subject: [PATCH 03/11] Introduced ES2015 Import/Export to resolve dependencies --- .../{rocket.js => RocketAdapter.js} | 7 ++----- .../rocketchat-slackbridge/{slack.js => SlackAdapter.js} | 7 ++----- packages/rocketchat-slackbridge/package.js | 7 ++----- packages/rocketchat-slackbridge/slackbridge.js | 9 ++++++--- 4 files changed, 12 insertions(+), 18 deletions(-) rename packages/rocketchat-slackbridge/{rocket.js => RocketAdapter.js} (99%) rename packages/rocketchat-slackbridge/{slack.js => SlackAdapter.js} (99%) diff --git a/packages/rocketchat-slackbridge/rocket.js b/packages/rocketchat-slackbridge/RocketAdapter.js similarity index 99% rename from packages/rocketchat-slackbridge/rocket.js rename to packages/rocketchat-slackbridge/RocketAdapter.js index bd97eb33d073..4b9f8f0002df 100644 --- a/packages/rocketchat-slackbridge/rocket.js +++ b/packages/rocketchat-slackbridge/RocketAdapter.js @@ -1,7 +1,6 @@ -/* globals logger SB_RocketAdapter */ -/* exported SB_RocketAdapter */ +/* globals logger*/ -class RocketAdapter { +export default class RocketAdapter { constructor(slackBridge) { logger.rocket.debug('constructor'); this.slackBridge = slackBridge; @@ -418,5 +417,3 @@ class RocketAdapter { } } - -SB_RocketAdapter = RocketAdapter; diff --git a/packages/rocketchat-slackbridge/slack.js b/packages/rocketchat-slackbridge/SlackAdapter.js similarity index 99% rename from packages/rocketchat-slackbridge/slack.js rename to packages/rocketchat-slackbridge/SlackAdapter.js index 44b304531dc5..10c7bb75205d 100644 --- a/packages/rocketchat-slackbridge/slack.js +++ b/packages/rocketchat-slackbridge/SlackAdapter.js @@ -1,7 +1,6 @@ -/* globals logger SB_SlackAdapter */ -/* exported SB_SlackAdapter */ +/* globals logger*/ -class SlackAdapter { +export default class SlackAdapter { constructor(slackBridge) { logger.slack.debug('constructor'); @@ -1081,5 +1080,3 @@ class SlackAdapter { } -SB_SlackAdapter = SlackAdapter; - diff --git a/packages/rocketchat-slackbridge/package.js b/packages/rocketchat-slackbridge/package.js index 9999c175905a..ed365ff8fb04 100644 --- a/packages/rocketchat-slackbridge/package.js +++ b/packages/rocketchat-slackbridge/package.js @@ -15,13 +15,10 @@ Package.onUse(function(api) { api.addFiles('logger.js', 'server'); api.addFiles('settings.js', 'server'); - api.addFiles('rocket.js', 'server'); - api.addFiles('slack.js', 'server'); + api.addFiles('RocketAdapter.js', 'server'); + api.addFiles('SlackAdapter.js', 'server'); api.addFiles('slackbridge.js', 'server'); api.addFiles('slashcommand/slackbridge_import.server.js', 'server'); - - api.export('SB_SlackAdapter', 'server'); - api.export('SB_RocketAdapter', 'server'); }); Npm.depends({ diff --git a/packages/rocketchat-slackbridge/slackbridge.js b/packages/rocketchat-slackbridge/slackbridge.js index 616e1760a33e..a6af9e44df55 100644 --- a/packages/rocketchat-slackbridge/slackbridge.js +++ b/packages/rocketchat-slackbridge/slackbridge.js @@ -1,4 +1,7 @@ -/* globals logger SB_SlackAdapter SB_RocketAdapter*/ +/* globals logger*/ + +import SlackAdapter from './SlackAdapter.js'; +import RocketAdapter from './RocketAdapter.js'; /** * SlackBridge interfaces between this Rocket installation and a remote Slack installation. @@ -6,8 +9,8 @@ class SlackBridge { constructor() { - this.slack = new SB_SlackAdapter(this); - this.rocket = new SB_RocketAdapter(this); + this.slack = new SlackAdapter(this); + this.rocket = new RocketAdapter(this); this.reactionsMap = new Map(); //Sync object between rocket and slack this.connected = false; this.rocket.setSlack(this.slack); From 743001e4256849f91d465ca4fc37e5af2a99e7c9 Mon Sep 17 00:00:00 2001 From: Hudell Date: Wed, 14 Mar 2018 17:02:00 -0300 Subject: [PATCH 04/11] Lint --- .../server/RocketAdapter.js | 16 +- .../server/SlackAdapter.js | 276 ++++++++++-------- .../server/slackbridge.js | 24 +- 3 files changed, 173 insertions(+), 143 deletions(-) diff --git a/packages/rocketchat-slackbridge/server/RocketAdapter.js b/packages/rocketchat-slackbridge/server/RocketAdapter.js index 640954fb25ab..f1f94416d410 100644 --- a/packages/rocketchat-slackbridge/server/RocketAdapter.js +++ b/packages/rocketchat-slackbridge/server/RocketAdapter.js @@ -1,5 +1,7 @@ /* globals logger*/ +import _ from 'underscore'; + export default class RocketAdapter { constructor(slackBridge) { logger.rocket.debug('constructor'); @@ -57,7 +59,7 @@ export default class RocketAdapter { logger.rocket.debug('onRocketSetReaction'); if (rocketMsgID && reaction) { - if (this.slackBridge.reactionsMap.delete('set' + rocketMsgID + reaction)) { + if (this.slackBridge.reactionsMap.delete(`set${ rocketMsgID }${ reaction }`)) { //This was a Slack reaction, we don't need to tell Slack about it return; } @@ -80,7 +82,7 @@ export default class RocketAdapter { logger.rocket.debug('onRocketUnSetReaction'); if (rocketMsgID && reaction) { - if (this.slackBridge.reactionsMap.delete('unset' + rocketMsgID + reaction)) { + if (this.slackBridge.reactionsMap.delete(`unset${ rocketMsgID }${ reaction }`)) { //This was a Slack unset reaction, we don't need to tell Slack about it return; } @@ -165,7 +167,7 @@ export default class RocketAdapter { } createRocketID(slackChannel, ts) { - return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`; + return `slack-${ slackChannel }-${ ts.replace(/\./g, '-') }`; } findChannel(slackChannelId) { @@ -243,7 +245,7 @@ export default class RocketAdapter { findUser(slackUserID) { const rocketUser = RocketChat.models.Users.findOneByImportId(slackUserID); if (rocketUser && !this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUser.username}` }; + this.userTags[slackUserID] = { slack: `<@${ slackUserID }>`, rocket: `@${ rocketUser.username }` }; } return rocketUser; } @@ -321,7 +323,7 @@ export default class RocketAdapter { } RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); if (!this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${slackUserID}>`, rocket: `@${rocketUserData.name}` }; + this.userTags[slackUserID] = { slack: `<@${ slackUserID }>`, rocket: `@${ rocketUserData.name }` }; } return RocketChat.models.Users.findOneById(rocketUserData.rocketId); } @@ -330,9 +332,9 @@ export default class RocketAdapter { } addAliasToMsg(rocketUserName, rocketMsgObj) { - var aliasFormat = RocketChat.settings.get('SlackBridge_AliasFormat'); + const aliasFormat = RocketChat.settings.get('SlackBridge_AliasFormat'); if (aliasFormat) { - var alias = this.util.format(aliasFormat, rocketUserName); + const alias = this.util.format(aliasFormat, rocketUserName); if (alias !== rocketUserName) { rocketMsgObj.alias = alias; diff --git a/packages/rocketchat-slackbridge/server/SlackAdapter.js b/packages/rocketchat-slackbridge/server/SlackAdapter.js index f5bc231b5a41..aa492971fcee 100644 --- a/packages/rocketchat-slackbridge/server/SlackAdapter.js +++ b/packages/rocketchat-slackbridge/server/SlackAdapter.js @@ -1,4 +1,9 @@ /* globals logger*/ +import _ from 'underscore'; +import url from 'url'; +import http from 'http'; +import https from 'https'; + export default class SlackAdapter { @@ -20,7 +25,7 @@ export default class SlackAdapter { connect(apiToken) { this.apiToken = apiToken; - var RtmClient = this.slackClient.RtmClient; + const RtmClient = this.slackClient.RtmClient; if (null != RtmClient) { RtmClient.disconnect; } @@ -326,7 +331,7 @@ export default class SlackAdapter { } if (rocketMsg && rocketUser) { - const rocketReaction = ':' + slackReactionMsg.reaction + ':'; + const rocketReaction = `:${ slackReactionMsg.reaction }:`; //If the Rocket user has already been removed, then this is an echo back from slack if (rocketMsg.reactions) { @@ -342,7 +347,7 @@ export default class SlackAdapter { } //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); + this.slackBridge.reactionsMap.set(`unset${ rocketMsg._id }${ rocketReaction }`, rocketUser); logger.slack.debug('Removing reaction from Slack'); Meteor.runAsUser(rocketUser._id, () => { Meteor.call('setReaction', rocketReaction, rocketMsg._id); @@ -372,7 +377,7 @@ export default class SlackAdapter { } if (rocketMsg && rocketUser) { - const rocketReaction = ':' + slackReactionMsg.reaction + ':'; + const rocketReaction = `:${ slackReactionMsg.reaction }:`; //If the Rocket user has already reacted, then this is Slack echoing back to us if (rocketMsg.reactions) { @@ -385,7 +390,7 @@ export default class SlackAdapter { } //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); + this.slackBridge.reactionsMap.set(`set${ rocketMsg._id }${ rocketReaction }`, rocketUser); logger.slack.debug('Adding reaction from Slack'); Meteor.runAsUser(rocketUser._id, () => { Meteor.call('setReaction', rocketReaction, rocketMsg._id); @@ -581,9 +586,9 @@ export default class SlackAdapter { postDeleteMessage(rocketMessage) { if (rocketMessage) { - var slackChannel = this.getSlackChannel(rocketMessage.rid); + const slackChannel = this.getSlackChannel(rocketMessage.rid); - if (null != slackChannel) { + if (slackChannel != null) { const data = { token: this.apiToken, ts: this.getTimeStamp(rocketMessage), @@ -618,7 +623,7 @@ export default class SlackAdapter { const postResult = HTTP.post('https://slack.com/api/chat.postMessage', { params: data }); if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); - logger.slack.debug('RocketMsgID=' + rocketMessage._id + ' SlackMsgID=' + postResult.data.message.ts + ' SlackBotID=' + postResult.data.message.bot_id); + logger.slack.debug(`RocketMsgID=${ rocketMessage._id } SlackMsgID=${ postResult.data.message.ts } SlackBotID=${ postResult.data.message.bot_id }`); } } } @@ -737,91 +742,156 @@ export default class SlackAdapter { } } + processBotMessage(rocketChannel, slackMessage) { + const excludeBotNames = RocketChat.settings.get('SlackBridge_Botnames'); + if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) { + return; + } + + const rocketMsgObj = { + msg: 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; + } + + processMeMessage(rocketUser, slackMessage) { + return this.rocket.addAliasToMsg(rocketUser.username, { + msg: `_${ this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text) }_` + }); + } + + processChannelJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + if (isImporting) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.addUserToRoom(rocketChannel._id, rocketUser); + } + } + + processGroupJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + if (slackMessage.inviter) { + const inviter = slackMessage.inviter ? this.rocket.findUser(slackMessage.inviter) || this.rocket.addUser(slackMessage.inviter) : null; + if (isImporting) { + RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, { + ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), + u: { + _id: inviter._id, + username: inviter.username + }, + imported: 'slackbridge' + }); + } else { + RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter); + } + } + } + + processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + if (isImporting) { + RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, { + ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), + imported: 'slackbridge' + }); + } else { + RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser); + } + } + + processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + if (isImporting) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false); + } + } + + processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + if (isImporting) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false); + } + } + + processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting) { + if (isImporting) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); + } else { + RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false); + } + } + + 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); + } + } + + 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' : this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text), + 'author_name' : slackMessage.attachments[0].author_subname, + 'author_icon' : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname), + 'ts' : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000) + }] + }; + + if (!isImporting) { + RocketChat.models.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; + } else { + logger.slack.error('Pinned item with no attachment'); + } + } processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) { - let rocketMsgObj = null; switch (slackMessage.subtype) { case 'bot_message': - var excludeBotNames = RocketChat.settings.get('SlackBridge_Botnames'); - if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) { - return; - } - - rocketMsgObj = { - msg: 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; + return this.processBotMessage(rocketChannel, slackMessage); case 'me_message': - return this.rocket.addAliasToMsg(rocketUser.username, { - msg: `_${this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text)}_` - }); + return this.processMeMessage(rocketUser, slackMessage); case 'channel_join': - if (isImporting) { - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.addUserToRoom(rocketChannel._id, rocketUser); - } - return; + return this.processChannelJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'group_join': - if (slackMessage.inviter) { - const inviter = slackMessage.inviter ? this.rocket.findUser(slackMessage.inviter) || this.rocket.addUser(slackMessage.inviter) : null; - if (isImporting) { - RocketChat.models.Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, { - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), - u: { - _id: inviter._id, - username: inviter.username - }, - imported: 'slackbridge' - }); - } else { - RocketChat.addUserToRoom(rocketChannel._id, rocketUser, inviter); - } - } - return; + return this.processGroupJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'channel_leave': case 'group_leave': - if (isImporting) { - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, { - ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), - imported: 'slackbridge' - }); - } else { - RocketChat.removeUserFromRoom(rocketChannel._id, rocketUser); - } - return; + return this.processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'channel_topic': case 'group_topic': - if (isImporting) { - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false); - } - return; + return this.processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'channel_purpose': case 'group_purpose': - if (isImporting) { - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false); - } - return; + return this.processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'channel_name': case 'group_name': - if (isImporting) { - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' }); - } else { - RocketChat.saveRoomName(rocketChannel._id, slackMessage.name, rocketUser, false); - } - return; + return this.processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'channel_archive': case 'group_archive': if (!isImporting) { @@ -835,17 +905,7 @@ export default class SlackAdapter { } return; case 'file_share': - 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); - } - break; + return this.processShareMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'file_comment': logger.slack.error('File comment not implemented'); return; @@ -853,32 +913,7 @@ export default class SlackAdapter { logger.slack.error('File mentioned not implemented'); return; case 'pinned_item': - if (slackMessage.attachments && slackMessage.attachments[0] && slackMessage.attachments[0].text) { - rocketMsgObj = { - rid: rocketChannel._id, - t: 'message_pinned', - msg: '', - u: { - _id: rocketUser._id, - username: rocketUser.username - }, - attachments: [{ - 'text' : this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.attachments[0].text), - 'author_name' : slackMessage.attachments[0].author_subname, - 'author_icon' : getAvatarUrlFromUsername(slackMessage.attachments[0].author_subname), - 'ts' : new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000) - }] - }; - - if (!isImporting) { - RocketChat.models.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; - } else { - logger.slack.error('Pinned item with no attachment'); - } - return; + return this.processPinnedItemMessage(rocketChannel, rocketUser, slackMessage, isImporting); case 'unpinned_item': logger.slack.error('Unpinned item not implemented'); return; @@ -896,11 +931,11 @@ export default class SlackAdapter { //details, slackMessage.file.url_private_download, rocketUser, rocketChannel, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), isImporting); uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) { const requestModule = /https/i.test(slackFileURL) ? https : http; - var parsedUrl = url.parse(slackFileURL, true); + const parsedUrl = url.parse(slackFileURL, true); parsedUrl.headers = { 'Authorization': `Bearer ${ this.apiToken }` }; requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => { const fileStore = FileUpload.getStore('Uploads'); - + fileStore.insert(details, stream, (err, file) => { if (err) { throw new Error(err); @@ -950,9 +985,8 @@ export default class SlackAdapter { return RocketChat.sendMessage(rocketUser, msg, rocketChannel, true); } }); - } - })); -} + })); + } importFromHistory(family, options) { logger.slack.debug('Importing messages history'); @@ -1022,7 +1056,7 @@ export default class SlackAdapter { if (pin.message) { const user = this.rocket.findUser(pin.message.user); const msgObj = { - rid: rid, + rid, t: 'message_pinned', msg: '', u: { diff --git a/packages/rocketchat-slackbridge/server/slackbridge.js b/packages/rocketchat-slackbridge/server/slackbridge.js index 87b176ce54d9..656257dd1e1e 100644 --- a/packages/rocketchat-slackbridge/server/slackbridge.js +++ b/packages/rocketchat-slackbridge/server/slackbridge.js @@ -1,9 +1,4 @@ /* globals logger */ -import _ from 'underscore'; -import util from 'util'; -import url from 'url'; -import http from 'http'; -import https from 'https'; import SlackAdapter from './SlackAdapter.js'; import RocketAdapter from './RocketAdapter.js'; @@ -58,35 +53,34 @@ class SlackBridge { } } - logger.class.debug('Setting: ' + key, value); - } - }); + logger.class.debug(`Setting: ${ key }`, value); + }); //Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used. RocketChat.settings.get('SlackBridge_AliasFormat', (key, value) => { this.aliasFormat = value; - logger.class.debug('Setting: ' + key, value); + logger.class.debug(`Setting: ${ key }`, value); }); //Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated. RocketChat.settings.get('SlackBridge_ExcludeBotnames', (key, value) => { this.excludeBotnames = value; - logger.class.debug('Setting: ' + key, value); + logger.class.debug(`Setting: ${ key }`, value); }); //Choose whether SlackBridge should also send your messages back to Slack RocketChat.settings.get('SlackBridge_Out_Enabled', (key, value) => { - logger.class.debug('Setting: ' + key, value); + logger.class.debug(`Setting: ${ key }`, value); }); //Send messages from all channels that exist in Slack and the bot has joined RocketChat.settings.get('SlackBridge_Out_All', (key, value) => { - logger.class.debug('Setting: ' + key, value); + logger.class.debug(`Setting: ${ key }`, value); }); //Choose which channels will send messages back to Slack RocketChat.settings.get('SlackBridge_Out_Channels', (key, value) => { - logger.class.debug('Setting: ' + key, value); + logger.class.debug(`Setting: ${ key }`, value); }); @@ -97,9 +91,9 @@ class SlackBridge { } else { this.disconnect(); } - logger.class.debug('Setting: ' + key, value); + logger.class.debug(`Setting: ${ key }`, value); }); } } -RocketChat.SlackBridge = new SlackBridge; \ No newline at end of file +RocketChat.SlackBridge = new SlackBridge; From d76149b08d0749332b15e9f48c38980f0b442294 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 22 May 2018 17:51:01 -0300 Subject: [PATCH 05/11] Fixed npm require --- packages/rocketchat-slackbridge/server/SlackAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-slackbridge/server/SlackAdapter.js b/packages/rocketchat-slackbridge/server/SlackAdapter.js index 219ccdd4fb58..a3068ae67cc0 100644 --- a/packages/rocketchat-slackbridge/server/SlackAdapter.js +++ b/packages/rocketchat-slackbridge/server/SlackAdapter.js @@ -10,7 +10,7 @@ export default class SlackAdapter { constructor(slackBridge) { logger.slack.debug('constructor'); this.slackBridge = slackBridge; - this.slackClient = Npm.require('@slack/client'); + this.slackClient = require('@slack/client'); 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 From 223bb6c0a12eae29fe2cb4df40bced1f96738400 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 13 Jun 2018 10:31:43 -0300 Subject: [PATCH 06/11] Removed npm.depends --- packages/rocketchat-slackbridge/package.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/rocketchat-slackbridge/package.js b/packages/rocketchat-slackbridge/package.js index 69e27876775b..6e9a043af299 100644 --- a/packages/rocketchat-slackbridge/package.js +++ b/packages/rocketchat-slackbridge/package.js @@ -19,7 +19,3 @@ Package.onUse(function(api) { api.addFiles('server/RocketAdapter.js', 'server'); api.addFiles('server/SlackAdapter.js', 'server'); }); - -Npm.depends({ - 'slack-client': '2.0.6' -}); From a0d911a047e300f5c2eed53bba1730a3eaa107bc Mon Sep 17 00:00:00 2001 From: Pierre Date: Fri, 15 Jun 2018 18:51:05 -0300 Subject: [PATCH 07/11] Started working on multiple slackbridges --- .../server/RocketAdapter.js | 53 +++++++++++-------- .../server/slackbridge.js | 2 +- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/rocketchat-slackbridge/server/RocketAdapter.js b/packages/rocketchat-slackbridge/server/RocketAdapter.js index f1f94416d410..15beba160862 100644 --- a/packages/rocketchat-slackbridge/server/RocketAdapter.js +++ b/packages/rocketchat-slackbridge/server/RocketAdapter.js @@ -9,7 +9,7 @@ export default class RocketAdapter { this.util = Npm.require('util'); this.userTags = {}; - this.slack = {}; + this.slackAdapters = []; } connect() { @@ -20,8 +20,10 @@ export default class RocketAdapter { this.unregisterForEvents(); } - setSlack(slack) { - this.slack = slack; + addSlack(slack) { + if (this.slackAdapters.indexOf(slack) < 0) { + this.slackAdapters.push(slack); + } } registerForEvents() { @@ -41,17 +43,18 @@ export default class RocketAdapter { } onMessageDelete(rocketMessageDeleted) { - try { - if (! this.slack.getSlackChannel(rocketMessageDeleted.rid)) { - //This is on a channel that the rocket bot is not subscribed - return; + this.slackAdapters.forEach((slack) => { + try { + if (!slack.getSlackChannel(rocketMessageDeleted.rid)) { + //This is on a channel that the rocket bot is not subscribed on this slack server + return; + } + logger.rocket.debug('onRocketMessageDelete', rocketMessageDeleted); + slack.postDeleteMessage(rocketMessageDeleted); + } catch (err) { + logger.rocket.error('Unhandled error onMessageDelete', err); } - logger.rocket.debug('onRocketMessageDelete', rocketMessageDeleted); - - this.slack.postDeleteMessage(rocketMessageDeleted); - } catch (err) { - logger.rocket.error('Unhandled error onMessageDelete', err); - } + }); } onSetReaction(rocketMsgID, reaction) { @@ -65,11 +68,13 @@ export default class RocketAdapter { } const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); if (rocketMsg) { - const slackChannel = this.slack.getSlackChannel(rocketMsg.rid); - if (null != slackChannel) { - const slackTS = this.slack.getTimeStamp(rocketMsg); - this.slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS); - } + this.slackAdapters.forEach((slack) => { + const slackChannel = slack.getSlackChannel(rocketMsg.rid); + if (null != slackChannel) { + const slackTS = slack.getTimeStamp(rocketMsg); + slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS); + } + }); } } } catch (err) { @@ -89,11 +94,13 @@ export default class RocketAdapter { const rocketMsg = RocketChat.models.Messages.findOneById(rocketMsgID); if (rocketMsg) { - const slackChannel = this.slack.getSlackChannel(rocketMsg.rid); - if (null != slackChannel) { - const slackTS = this.slack.getTimeStamp(rocketMsg); - this.slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS); - } + this.slackAdapters.forEach((slack) => { + const slackChannel = slack.getSlackChannel(rocketMsg.rid); + if (null != slackChannel) { + const slackTS = slack.getTimeStamp(rocketMsg); + slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS); + } + }); } } } catch (err) { diff --git a/packages/rocketchat-slackbridge/server/slackbridge.js b/packages/rocketchat-slackbridge/server/slackbridge.js index 656257dd1e1e..d67da3c220cb 100644 --- a/packages/rocketchat-slackbridge/server/slackbridge.js +++ b/packages/rocketchat-slackbridge/server/slackbridge.js @@ -14,7 +14,7 @@ class SlackBridge { this.reactionsMap = new Map(); //Sync object between rocket and slack this.connected = false; - this.rocket.setSlack(this.slack); + this.rocket.addSlack(this.slack); this.slack.setRocket(this.rocket); this.processSettings(); From a4e1bff057cdf1361160669ba089918b779b72dc Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Jun 2018 10:33:36 -0300 Subject: [PATCH 08/11] Multiple slack bridges --- .../server/RocketAdapter.js | 317 ++++++++++-------- .../server/SlackAdapter.js | 2 +- 2 files changed, 172 insertions(+), 147 deletions(-) diff --git a/packages/rocketchat-slackbridge/server/RocketAdapter.js b/packages/rocketchat-slackbridge/server/RocketAdapter.js index 15beba160862..905bde9dd654 100644 --- a/packages/rocketchat-slackbridge/server/RocketAdapter.js +++ b/packages/rocketchat-slackbridge/server/RocketAdapter.js @@ -96,7 +96,7 @@ export default class RocketAdapter { if (rocketMsg) { this.slackAdapters.forEach((slack) => { const slackChannel = slack.getSlackChannel(rocketMsg.rid); - if (null != slackChannel) { + if (slackChannel != null) { const slackTS = slack.getTimeStamp(rocketMsg); slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS); } @@ -109,48 +109,51 @@ export default class RocketAdapter { } onMessage(rocketMessage) { - try { - if (! this.slack.getSlackChannel(rocketMessage.rid)) { - //This is on a channel that the rocket bot is not subscribed - return; - } - logger.rocket.debug('onRocketMessage', rocketMessage); + this.slackAdapters.forEach((slack) => { + try { + if (! slack.getSlackChannel(rocketMessage.rid)) { + //This is on a channel that the rocket bot is not subscribed + return; + } + logger.rocket.debug('onRocketMessage', rocketMessage); - if (rocketMessage.editedAt) { - //This is an Edit Event - this.processMessageChanged(rocketMessage); - return rocketMessage; - } - // Ignore messages originating from Slack - if (rocketMessage._id.indexOf('slack-') === 0) { - return rocketMessage; - } + if (rocketMessage.editedAt) { + //This is an Edit Event + this.processMessageChanged(rocketMessage, slack); + return; + } + // Ignore messages originating from Slack + if (rocketMessage._id.indexOf('slack-') === 0) { + return; + } - //A new message from Rocket.Chat - this.processSendMessage(rocketMessage); - } catch (err) { - logger.rocket.error('Unhandled error onMessage', err); - } + //A new message from Rocket.Chat + this.processSendMessage(rocketMessage, slack); + + } catch (err) { + logger.rocket.error('Unhandled error onMessage', err); + } + }); return rocketMessage; } - processSendMessage(rocketMessage) { + processSendMessage(rocketMessage, slack) { //Since we got this message, SlackBridge_Out_Enabled is true if (RocketChat.settings.get('SlackBridge_Out_All') === true) { - this.slack.postMessage(this.slack.getSlackChannel(rocketMessage.rid), rocketMessage); + slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage); } else { //They want to limit to certain groups const outSlackChannels = _.pluck(RocketChat.settings.get('SlackBridge_Out_Channels'), '_id') || []; //logger.rocket.debug('Out SlackChannels: ', outSlackChannels); if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) { - this.slack.postMessage(this.slack.getSlackChannel(rocketMessage.rid), rocketMessage); + slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage); } } } - processMessageChanged(rocketMessage) { + processMessageChanged(rocketMessage, slack) { if (rocketMessage) { if (rocketMessage.updatedBySlack) { //We have already processed this @@ -159,8 +162,8 @@ export default class RocketAdapter { } //This was a change from Rocket.Chat - const slackChannel = this.slack.getSlackChannel(rocketMessage.rid); - this.slack.postMessageUpdate(slackChannel, rocketMessage); + const slackChannel = slack.getSlackChannel(rocketMessage.rid); + slack.postMessageUpdate(slackChannel, rocketMessage); } } @@ -183,70 +186,82 @@ export default class RocketAdapter { addChannel(slackChannelID, hasRetried = false) { logger.rocket.debug('Adding Rocket.Chat channel from Slack', slackChannelID); - let slackResults = null; - let isGroup = false; - if (slackChannelID.charAt(0) === 'C') { - slackResults = HTTP.get('https://slack.com/api/channels.info', { params: { token: this.slackBridge.apiToken, channel: slackChannelID } }); - } else if (slackChannelID.charAt(0) === 'G') { - slackResults = HTTP.get('https://slack.com/api/groups.info', { params: { token: this.slackBridge.apiToken, channel: slackChannelID } }); - isGroup = true; - } - if (slackResults && slackResults.data && slackResults.data.ok === true) { - const rocketChannelData = isGroup ? slackResults.data.group : slackResults.data.channel; - const existingRocketRoom = RocketChat.models.Rooms.findOneByName(rocketChannelData.name); - - // If the room exists, make sure we have its id in importIds - if (existingRocketRoom || rocketChannelData.is_general) { - rocketChannelData.rocketId = rocketChannelData.is_general ? 'GENERAL' : existingRocketRoom._id; - RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); - } else { - const rocketUsers = []; - for (const member of rocketChannelData.members) { - if (member !== rocketChannelData.creator) { - const rocketUser = this.findUser(member) || this.addUser(member); - if (rocketUser && rocketUser.username) { - rocketUsers.push(rocketUser.username); + let addedRoom; + + this.slackAdapters.forEach((slack) => { + if (addedRoom) { + return; + } + + let slackResults = null; + let isGroup = false; + if (slackChannelID.charAt(0) === 'C') { + slackResults = HTTP.get('https://slack.com/api/channels.info', { params: { token: slack.apiToken, channel: slackChannelID } }); + } else if (slackChannelID.charAt(0) === 'G') { + slackResults = HTTP.get('https://slack.com/api/groups.info', { params: { token: slack.apiToken, channel: slackChannelID } }); + isGroup = true; + } + if (slackResults && slackResults.data && slackResults.data.ok === true) { + const rocketChannelData = isGroup ? slackResults.data.group : slackResults.data.channel; + const existingRocketRoom = RocketChat.models.Rooms.findOneByName(rocketChannelData.name); + + // If the room exists, make sure we have its id in importIds + if (existingRocketRoom || rocketChannelData.is_general) { + rocketChannelData.rocketId = rocketChannelData.is_general ? 'GENERAL' : existingRocketRoom._id; + RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); + } else { + const rocketUsers = []; + for (const member of rocketChannelData.members) { + if (member !== rocketChannelData.creator) { + const rocketUser = this.findUser(member) || this.addUser(member); + if (rocketUser && rocketUser.username) { + rocketUsers.push(rocketUser.username); + } } } - } - const rocketUserCreator = rocketChannelData.creator ? this.findUser(rocketChannelData.creator) || this.addUser(rocketChannelData.creator) : null; - if (!rocketUserCreator) { - logger.rocket.error('Could not fetch room creator information', rocketChannelData.creator); - return; - } + const rocketUserCreator = rocketChannelData.creator ? this.findUser(rocketChannelData.creator) || this.addUser(rocketChannelData.creator) : null; + if (!rocketUserCreator) { + logger.rocket.error('Could not fetch room creator information', rocketChannelData.creator); + return; + } - try { - const rocketChannel = RocketChat.createRoom(isGroup ? 'p' : 'c', rocketChannelData.name, rocketUserCreator.username, rocketUsers); - rocketChannelData.rocketId = rocketChannel.rid; - } catch (e) { - if (!hasRetried) { - logger.rocket.debug('Error adding channel from Slack. Will retry in 1s.', e.message); - // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. - Meteor._sleepForMs(1000); - return this.findChannel(slackChannelID) || this.addChannel(slackChannelID, true); - } else { - console.log(e.message); + try { + const rocketChannel = RocketChat.createRoom(isGroup ? 'p' : 'c', rocketChannelData.name, rocketUserCreator.username, rocketUsers); + rocketChannelData.rocketId = rocketChannel.rid; + } catch (e) { + if (!hasRetried) { + logger.rocket.debug('Error adding channel from Slack. Will retry in 1s.', e.message); + // If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s. + Meteor._sleepForMs(1000); + return this.findChannel(slackChannelID) || this.addChannel(slackChannelID, true); + } else { + console.log(e.message); + } } - } - const roomUpdate = { - ts: new Date(rocketChannelData.created * 1000) - }; - let lastSetTopic = 0; - if (!_.isEmpty(rocketChannelData.topic && rocketChannelData.topic.value)) { - roomUpdate.topic = rocketChannelData.topic.value; - lastSetTopic = rocketChannelData.topic.last_set; - } - if (!_.isEmpty(rocketChannelData.purpose && rocketChannelData.purpose.value) && rocketChannelData.purpose.last_set > lastSetTopic) { - roomUpdate.topic = rocketChannelData.purpose.value; + const roomUpdate = { + ts: new Date(rocketChannelData.created * 1000) + }; + let lastSetTopic = 0; + if (!_.isEmpty(rocketChannelData.topic && rocketChannelData.topic.value)) { + roomUpdate.topic = rocketChannelData.topic.value; + lastSetTopic = rocketChannelData.topic.last_set; + } + if (!_.isEmpty(rocketChannelData.purpose && rocketChannelData.purpose.value) && rocketChannelData.purpose.last_set > lastSetTopic) { + roomUpdate.topic = rocketChannelData.purpose.value; + } + RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); + slack.addSlackChannel(rocketChannelData.rocketId, slackChannelID); } - RocketChat.models.Rooms.addImportIds(rocketChannelData.rocketId, rocketChannelData.id); - this.slack.addSlackChannel(rocketChannelData.rocketId, slackChannelID); + addedRoom = RocketChat.models.Rooms.findOneById(rocketChannelData.rocketId); } - return RocketChat.models.Rooms.findOneById(rocketChannelData.rocketId); + }); + + if (!addedRoom) { + logger.rocket.debug('Channel not added'); } - logger.rocket.debug('Channel not added'); - return; + + return addedRoom; } findUser(slackUserID) { @@ -259,83 +274,93 @@ export default class RocketAdapter { addUser(slackUserID) { logger.rocket.debug('Adding Rocket.Chat user from Slack', slackUserID); - const slackResults = HTTP.get('https://slack.com/api/users.info', { params: { token: this.slackBridge.apiToken, user: slackUserID } }); - if (slackResults && slackResults.data && slackResults.data.ok === true && slackResults.data.user) { - const rocketUserData = slackResults.data.user; - const isBot = rocketUserData.is_bot === true; - const email = rocketUserData.profile && rocketUserData.profile.email || ''; - let existingRocketUser; - if (!isBot) { - existingRocketUser = RocketChat.models.Users.findOneByEmailAddress(email) || RocketChat.models.Users.findOneByUsername(rocketUserData.name); - } else { - existingRocketUser = RocketChat.models.Users.findOneByUsername(rocketUserData.name); + let addedUser; + this.slackAdapters.forEach((slack) => { + if (addedUser) { + return; } - if (existingRocketUser) { - rocketUserData.rocketId = existingRocketUser._id; - rocketUserData.name = existingRocketUser.username; - } else { - const newUser = { - password: Random.id(), - username: rocketUserData.name - }; - - if (!isBot && email) { - newUser.email = email; + const slackResults = HTTP.get('https://slack.com/api/users.info', { params: { token: slack.apiToken, user: slackUserID } }); + if (slackResults && slackResults.data && slackResults.data.ok === true && slackResults.data.user) { + const rocketUserData = slackResults.data.user; + const isBot = rocketUserData.is_bot === true; + const email = rocketUserData.profile && rocketUserData.profile.email || ''; + let existingRocketUser; + if (!isBot) { + existingRocketUser = RocketChat.models.Users.findOneByEmailAddress(email) || RocketChat.models.Users.findOneByUsername(rocketUserData.name); + } else { + existingRocketUser = RocketChat.models.Users.findOneByUsername(rocketUserData.name); } - if (isBot) { - newUser.joinDefaultChannels = false; - } + if (existingRocketUser) { + rocketUserData.rocketId = existingRocketUser._id; + rocketUserData.name = existingRocketUser.username; + } else { + const newUser = { + password: Random.id(), + username: rocketUserData.name + }; + + if (!isBot && email) { + newUser.email = email; + } - rocketUserData.rocketId = Accounts.createUser(newUser); - const userUpdate = { - utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, - roles: isBot ? [ 'bot' ] : [ 'user' ] - }; + if (isBot) { + newUser.joinDefaultChannels = false; + } - if (rocketUserData.profile && rocketUserData.profile.real_name) { - userUpdate['name'] = rocketUserData.profile.real_name; - } + rocketUserData.rocketId = Accounts.createUser(newUser); + const userUpdate = { + utcOffset: rocketUserData.tz_offset / 3600, // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600, + roles: isBot ? [ 'bot' ] : [ 'user' ] + }; - if (rocketUserData.deleted) { - userUpdate['active'] = false; - userUpdate['services.resume.loginTokens'] = []; - } + if (rocketUserData.profile && rocketUserData.profile.real_name) { + userUpdate['name'] = rocketUserData.profile.real_name; + } - RocketChat.models.Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate }); + if (rocketUserData.deleted) { + userUpdate['active'] = false; + userUpdate['services.resume.loginTokens'] = []; + } + + RocketChat.models.Users.update({ _id: rocketUserData.rocketId }, { $set: userUpdate }); - const user = RocketChat.models.Users.findOneById(rocketUserData.rocketId); + const user = RocketChat.models.Users.findOneById(rocketUserData.rocketId); - let url = null; - if (rocketUserData.profile) { - if (rocketUserData.profile.image_original) { - url = rocketUserData.profile.image_original; - } else if (rocketUserData.profile.image_512) { - url = rocketUserData.profile.image_512; + let url = null; + if (rocketUserData.profile) { + if (rocketUserData.profile.image_original) { + url = rocketUserData.profile.image_original; + } else if (rocketUserData.profile.image_512) { + url = rocketUserData.profile.image_512; + } } - } - if (url) { - try { - RocketChat.setUserAvatar(user, url, null, 'url'); - } catch (error) { - logger.rocket.debug('Error setting user avatar', error.message); + if (url) { + try { + RocketChat.setUserAvatar(user, url, null, 'url'); + } catch (error) { + logger.rocket.debug('Error setting user avatar', error.message); + } } } - } - const importIds = [ rocketUserData.id ]; - if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) { - importIds.push(rocketUserData.profile.bot_id); - } - RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); - if (!this.userTags[slackUserID]) { - this.userTags[slackUserID] = { slack: `<@${ slackUserID }>`, rocket: `@${ rocketUserData.name }` }; + const importIds = [ rocketUserData.id ]; + if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) { + importIds.push(rocketUserData.profile.bot_id); + } + RocketChat.models.Users.addImportIds(rocketUserData.rocketId, importIds); + if (!this.userTags[slackUserID]) { + this.userTags[slackUserID] = { slack: `<@${ slackUserID }>`, rocket: `@${ rocketUserData.name }` }; + } + addedUser = RocketChat.models.Users.findOneById(rocketUserData.rocketId); } - return RocketChat.models.Users.findOneById(rocketUserData.rocketId); + }); + + if (!addedUser) { + logger.rocket.debug('User not added'); } - logger.rocket.debug('User not added'); - return; + return addedUser; } addAliasToMsg(rocketUserName, rocketMsgObj) { @@ -351,11 +376,11 @@ export default class RocketAdapter { return rocketMsgObj; } - createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting) { + createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting, slack) { if (slackMessage.type === 'message') { let rocketMsgObj = {}; if (!_.isEmpty(slackMessage.subtype)) { - rocketMsgObj = this.slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting); + rocketMsgObj = slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting); if (!rocketMsgObj) { return; } diff --git a/packages/rocketchat-slackbridge/server/SlackAdapter.js b/packages/rocketchat-slackbridge/server/SlackAdapter.js index 7dc9006473d4..42752b206480 100644 --- a/packages/rocketchat-slackbridge/server/SlackAdapter.js +++ b/packages/rocketchat-slackbridge/server/SlackAdapter.js @@ -726,7 +726,7 @@ export default class SlackAdapter { msgDataDefaults['imported'] = 'slackbridge'; } try { - this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting); + this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, isImporting, this); } catch (e) { // http://www.mongodb.org/about/contributors/error-codes/ // 11000 == duplicate key error From f67adeef2c770347e9b9084ac30e2d11fbb023b7 Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 19 Jun 2018 13:30:15 -0300 Subject: [PATCH 09/11] fix to avoid receiving back the messages that we send to slack --- .../server/RocketAdapter.js | 7 ++- .../server/SlackAdapter.js | 54 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/rocketchat-slackbridge/server/RocketAdapter.js b/packages/rocketchat-slackbridge/server/RocketAdapter.js index 905bde9dd654..9f2fc0dc3de5 100644 --- a/packages/rocketchat-slackbridge/server/RocketAdapter.js +++ b/packages/rocketchat-slackbridge/server/RocketAdapter.js @@ -411,8 +411,11 @@ export default class RocketAdapter { } if (slackMessage.subtype === 'bot_message') { Meteor.setTimeout(() => { - if (slackMessage.bot_id && slackMessage.ts && !RocketChat.models.Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) { - RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); + if (slackMessage.bot_id && slackMessage.ts) { + //Make sure that a message with the same bot_id and timestamp doesn't already exists + if (!RocketChat.models.Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts)) { + RocketChat.sendMessage(rocketUser, rocketMsgObj, rocketChannel, true); + } } }, 500); } else { diff --git a/packages/rocketchat-slackbridge/server/SlackAdapter.js b/packages/rocketchat-slackbridge/server/SlackAdapter.js index 42752b206480..17158cb33126 100644 --- a/packages/rocketchat-slackbridge/server/SlackAdapter.js +++ b/packages/rocketchat-slackbridge/server/SlackAdapter.js @@ -16,6 +16,8 @@ export default class SlackAdapter { //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; } /** @@ -602,6 +604,35 @@ export default class SlackAdapter { } } + 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; + }); + } + postMessage(slackChannel, rocketMessage) { if (slackChannel && slackChannel.id) { let iconUrl = getAvatarUrlFromUsername(rocketMessage.u && rocketMessage.u.username); @@ -617,8 +648,20 @@ export default class SlackAdapter { link_names: 1 }; logger.slack.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 = HTTP.post('https://slack.com/api/chat.postMessage', { params: data }); + + if (!this.slackBotId && this.rocket.slackAdapters && this.rocket.slackAdapters.length >= 2) { + this.removeMessageBeingSent(data); + } + if (postResult.statusCode === 200 && postResult.data && postResult.data.message && postResult.data.message.bot_id && postResult.data.message.ts) { + this.slackBotId = postResult.data.message.bot_id; RocketChat.models.Messages.setSlackBotIdAndSlackTs(rocketMessage._id, postResult.data.message.bot_id, postResult.data.message.ts); logger.slack.debug(`RocketMsgID=${ rocketMessage._id } SlackMsgID=${ postResult.data.message.ts } SlackBotID=${ postResult.data.message.bot_id }`); } @@ -745,6 +788,17 @@ export default class SlackAdapter { 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: this.rocket.convertSlackMsgTxtToRocketTxtFormat(slackMessage.text), rid: rocketChannel._id, From 0c10f94939d12dddeee5ec4127fe5dc0ef3206f6 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 3 Oct 2018 15:30:49 -0300 Subject: [PATCH 10/11] Solved merge errors --- packages/rocketchat-slackbridge/tests/manual-tests.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/rocketchat-slackbridge/tests/manual-tests.txt b/packages/rocketchat-slackbridge/tests/manual-tests.txt index d05397b8e4a5..d43879a6f847 100644 --- a/packages/rocketchat-slackbridge/tests/manual-tests.txt +++ b/packages/rocketchat-slackbridge/tests/manual-tests.txt @@ -50,14 +50,11 @@ Channel exists on both w/ rocketbot ** Send message from slack ** Send message from rocket -<<<<<<< HEAD -======= Reactions: * Disabled (default) ** Try to react to Slack and Rocket messages * Enable it ** Try to react to Slack and Rocket messages ->>>>>>> develop From c574eff7679f0cb4d49a5ed5dd9db3eb62995e6b Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 3 Oct 2018 17:09:48 -0300 Subject: [PATCH 11/11] Changed the ApiToken setting to multine --- packages/rocketchat-i18n/i18n/en.i18n.json | 2 ++ .../server/RocketAdapter.js | 4 +++ .../rocketchat-slackbridge/server/settings.js | 4 ++- .../server/slackbridge.js | 33 ++++++++++++------- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 04e33259f09f..fd7a4ab3df54 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2407,6 +2407,8 @@ "Size": "Size", "Skip": "Skip", "Slack_Users": "Slack's Users CSV", + "SlackBridge_APIToken": "API Tokens", + "SlackBridge_APIToken_Description": "You can configure multiple slack servers by adding one API Token per line.", "SlackBridge_error": "SlackBridge got an error while importing your messages at %s: %s", "SlackBridge_finish": "SlackBridge has finished importing the messages at %s. Please reload to view all messages.", "SlackBridge_Out_All": "SlackBridge Out All", diff --git a/packages/rocketchat-slackbridge/server/RocketAdapter.js b/packages/rocketchat-slackbridge/server/RocketAdapter.js index cd123033ecc5..71faaf3d1106 100644 --- a/packages/rocketchat-slackbridge/server/RocketAdapter.js +++ b/packages/rocketchat-slackbridge/server/RocketAdapter.js @@ -26,6 +26,10 @@ export default class RocketAdapter { } } + clearSlackAdapters() { + this.slackAdapters = []; + } + registerForEvents() { logger.rocket.debug('Register for events'); RocketChat.callbacks.add('afterSaveMessage', this.onMessage.bind(this), RocketChat.callbacks.priority.LOW, 'SlackBridge_Out'); diff --git a/packages/rocketchat-slackbridge/server/settings.js b/packages/rocketchat-slackbridge/server/settings.js index 3105c6410fd1..ed00aa44ee12 100644 --- a/packages/rocketchat-slackbridge/server/settings.js +++ b/packages/rocketchat-slackbridge/server/settings.js @@ -8,11 +8,13 @@ Meteor.startup(function() { this.add('SlackBridge_APIToken', '', { type: 'string', + multiline: true, enableQuery: { _id: 'SlackBridge_Enabled', value: true, }, - i18nLabel: 'API_Token', + i18nLabel: 'SlackBridge_APIToken', + i18nDescription: 'SlackBridge_APIToken_Description', }); this.add('SlackBridge_FileUpload_Enabled', true, { diff --git a/packages/rocketchat-slackbridge/server/slackbridge.js b/packages/rocketchat-slackbridge/server/slackbridge.js index 73fcdaf26dd5..8a3d9d8609d5 100644 --- a/packages/rocketchat-slackbridge/server/slackbridge.js +++ b/packages/rocketchat-slackbridge/server/slackbridge.js @@ -7,19 +7,16 @@ import RocketAdapter from './RocketAdapter.js'; * SlackBridge interfaces between this Rocket installation and a remote Slack installation. */ class SlackBridge { - constructor() { - this.slack = new SlackAdapter(this); + this.slackAdapters = []; this.rocket = new RocketAdapter(this); this.reactionsMap = new Map(); // Sync object between rocket and slack this.connected = false; - this.rocket.addSlack(this.slack); - this.slack.setRocket(this.rocket); - + this.rocket.clearSlackAdapters(); // Settings that we cache versus looking up at runtime - this.apiToken = false; + this.apiTokens = false; this.aliasFormat = ''; this.excludeBotnames = ''; this.isReactionsEnabled = true; @@ -28,8 +25,19 @@ class SlackBridge { connect() { if (this.connected === false) { + this.slackAdapters = []; + this.rocket.clearSlackAdapters(); + + const tokenList = this.apiTokens.split('\n'); + tokenList.forEach((apiToken) => { + const slack = new SlackAdapter(this); + slack.setRocket(this.rocket); + this.rocket.addSlack(slack); + this.slackAdapters.push(slack); + + slack.connect(apiToken); + }); - this.slack.connect(this.apiToken); if (RocketChat.settings.get('SlackBridge_Out_Enabled')) { this.rocket.connect(); } @@ -42,7 +50,10 @@ class SlackBridge { disconnect() { if (this.connected === true) { this.rocket.disconnect(); - this.slack.disconnect(); + this.slackAdapters.forEach((slack) => { + slack.disconnect(); + }); + this.slackAdapters = []; this.connected = false; logger.connection.info('Disabled'); } @@ -51,8 +62,8 @@ class SlackBridge { processSettings() { // Slack installation API token RocketChat.settings.get('SlackBridge_APIToken', (key, value) => { - if (value !== this.apiToken) { - this.apiToken = value; + if (value !== this.apiTokens) { + this.apiTokens = value; if (this.connected) { this.disconnect(); this.connect(); @@ -82,7 +93,7 @@ class SlackBridge { // Is this entire SlackBridge enabled RocketChat.settings.get('SlackBridge_Enabled', (key, value) => { - if (value && this.apiToken) { + if (value && this.apiTokens) { this.connect(); } else { this.disconnect();