The communications platform that puts data protection first.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Rocket.Chat/app/slackbridge/server/SlackAdapter.js

1216 lines
37 KiB

import url from 'url';
import http from 'http';
import https from 'https';
import { RTMClient } from '@slack/client';
import { Meteor } from 'meteor/meteor';
import { logger } from './logger';
import { SlackAPI } from './SlackAPI';
import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL';
import { Messages, Rooms, Users } from '../../models';
import { settings } from '../../settings';
import {
deleteMessage,
updateMessage,
addUserToRoom,
removeUserFromRoom,
archiveRoom,
unarchiveRoom,
sendMessage,
} from '../../lib';
import { saveRoomName, saveRoomTopic } from '../../channel-settings';
import { FileUpload } from '../../file-upload';
export default class SlackAdapter {
constructor(slackBridge) {
logger.slack.debug('constructor');
this.slackBridge = slackBridge;
this.rtm = {}; // slack-client Real Time Messaging API
this.apiToken = {}; // Slack API Token passed in via Connect
// On Slack, a rocket integration bot will be added to slack channels, this is the list of those channels, key is Rocket Ch ID
this.slackChannelRocketBotMembershipMap = new Map(); // Key=RocketChannelID, Value=SlackChannel
this.rocket = {};
this.messagesBeingSent = [];
this.slackBotId = false;
this.slackAPI = {};
}
/**
* Connect to the remote Slack server using the passed in token API and register for Slack events
* @param apiToken
*/
connect(apiToken) {
this.apiToken = apiToken;
if (RTMClient != null) {
RTMClient.disconnect;
}
this.slackAPI = new SlackAPI(this.apiToken);
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');
this.rtm.on('authenticated', () => {
logger.slack.info('Connected to Slack');
});
this.rtm.on('unable_to_rtm_start', () => {
this.slackBridge.disconnect();
});
this.rtm.on('disconnected', () => {
logger.slack.info('Disconnected from Slack');
this.slackBridge.disconnect();
});
/**
* Event fired when someone messages a channel the bot is in
* {
* type: 'message',
* channel: [channel_id],
* user: [user_id],
* text: [message],
* ts: [ts.milli],
* team: [team_id],
* subtype: [message_subtype],
* inviter: [message_subtype = 'group_join|channel_join' -> user_id]
* }
**/
this.rtm.on('message', 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('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('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('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('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('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('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('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('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('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('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('team_join', Meteor.bindEnvironment(() => {}));
}
/*
https://api.slack.com/events/reaction_removed
*/
onReactionRemoved(slackReactionMsg) {
if (slackReactionMsg) {
if (!this.slackBridge.isReactionsEnabled) {
return;
}
const rocketUser = this.rocket.getUser(slackReactionMsg.user);
// Lets find our Rocket originated message
let rocketMsg = Messages.findOneBySlackTs(slackReactionMsg.item.ts);
if (!rocketMsg) {
// Must have originated from Slack
const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts);
rocketMsg = 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) {
if (!this.slackBridge.isReactionsEnabled) {
return;
}
const rocketUser = this.rocket.getUser(slackReactionMsg.user);
if (rocketUser.roles.includes('bot')) {
return;
}
// Lets find our Rocket originated message
let rocketMsg = Messages.findOneBySlackTs(slackReactionMsg.item.ts);
if (!rocketMsg) {
// Must have originated from Slack
const rocketID = this.rocket.createRocketID(slackReactionMsg.item.channel, slackReactionMsg.item.ts);
rocketMsg = 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) {
const isAFileShare = slackMessage && slackMessage.files && Array.isArray(slackMessage.files) && slackMessage.files.length;
if (isAFileShare) {
this.processFileShare(slackMessage);
return;
}
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);
}
}
postFindChannel(rocketChannelName) {
logger.slack.debug('Searching for Slack channel or group', rocketChannelName);
const channels = this.slackAPI.getChannels();
if (channels && channels.length > 0) {
for (const channel of channels) {
if (channel.name === rocketChannelName && channel.is_member === true) {
return channel;
}
}
}
const groups = this.slackAPI.getGroups();
if (groups && groups.length > 0) {
for (const group of groups) {
if (group.name === rocketChannelName) {
return group;
}
}
}
}
/**
* Retrieves the Slack TS from a Rocket msg that originated from Slack
* @param rocketMsg
* @returns Slack TS or undefined if not a message that originated from slack
* @private
*/
getTimeStamp(rocketMsg) {
// slack-G3KJGGE15-1483081061-000169
let slackTS;
let index = rocketMsg._id.indexOf('slack-');
if (index === 0) {
// This is a msg that originated from Slack
slackTS = rocketMsg._id.substr(6, rocketMsg._id.length);
index = slackTS.indexOf('-');
slackTS = slackTS.substr(index + 1, slackTS.length);
slackTS = slackTS.replace('-', '.');
} else {
// This probably originated as a Rocket msg, but has been sent to Slack
slackTS = rocketMsg.slackTs;
}
return slackTS;
}
/**
* Adds a slack channel to our collection that the rocketbot is a member of on slack
* @param rocketChID
* @param slackChID
*/
addSlackChannel(rocketChID, slackChID) {
const ch = this.getSlackChannel(rocketChID);
if (ch == null) {
logger.slack.debug('Added channel', { rocketChID, slackChID });
this.slackChannelRocketBotMembershipMap.set(rocketChID, { id: slackChID, family: slackChID.charAt(0) === 'C' ? 'channels' : 'groups' });
}
}
removeSlackChannel(slackChID) {
const keys = this.slackChannelRocketBotMembershipMap.keys();
let slackChannel;
let key;
while ((key = keys.next().value) != null) {
slackChannel = this.slackChannelRocketBotMembershipMap.get(key);
if (slackChannel.id === slackChID) {
// Found it, need to delete it
this.slackChannelRocketBotMembershipMap.delete(key);
break;
}
}
}
getSlackChannel(rocketChID) {
return this.slackChannelRocketBotMembershipMap.get(rocketChID);
}
populateMembershipChannelMapByChannels() {
const channels = this.slackAPI.getChannels();
if (!channels || channels.length <= 0) {
return;
}
for (const slackChannel of channels) {
const rocketchat_room = Rooms.findOneByName(slackChannel.name, { fields: { _id: 1 } }) || Rooms.findOneByImportId(slackChannel.id, { fields: { _id: 1 } });
if (rocketchat_room && slackChannel.is_member) {
this.addSlackChannel(rocketchat_room._id, slackChannel.id);
}
}
}
populateMembershipChannelMapByGroups() {
const groups = this.slackAPI.getGroups();
if (!groups || groups.length <= 0) {
return;
}
for (const slackGroup of groups) {
const rocketchat_room = Rooms.findOneByName(slackGroup.name, { fields: { _id: 1 } }) || Rooms.findOneByImportId(slackGroup.id, { fields: { _id: 1 } });
if (rocketchat_room && 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 = this.slackAPI.react(data);
if (postResult) {
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 = this.slackAPI.removeReaction(data);
if (postResult) {
logger.slack.debug('Reaction removed from Slack');
}
}
}
postDeleteMessage(rocketMessage) {
if (rocketMessage) {
const slackChannel = this.getSlackChannel(rocketMessage.rid);
if (slackChannel != null) {
const data = {
token: this.apiToken,
ts: this.getTimeStamp(rocketMessage),
channel: this.getSlackChannel(rocketMessage.rid).id,
as_user: true,
};
logger.slack.debug('Post Delete Message to Slack', data);
const postResult = this.slackAPI.removeMessage(data);
if (postResult) {
logger.slack.debug('Message deleted on Slack');
}
}
}
}
storeMessageBeingSent(data) {
this.messagesBeingSent.push(data);
}
removeMessageBeingSent(data) {
const idx = this.messagesBeingSent.indexOf(data);
if (idx >= 0) {
this.messagesBeingSent.splice(idx, 1);
}
}
isMessageBeingSent(username, channel) {
if (!this.messagesBeingSent.length) {
return false;
}
return this.messagesBeingSent.some((messageData) => {
if (messageData.username !== username) {
return false;
}
if (messageData.channel !== channel) {
return false;
}
return true;
});
}
postMessage(slackChannel, rocketMessage) {
if (slackChannel && slackChannel.id) {
let iconUrl = getUserAvatarURL(rocketMessage.u && rocketMessage.u.username);
if (iconUrl) {
iconUrl = Meteor.absoluteUrl().replace(/\/$/, '') + iconUrl;
}
const data = {
token: this.apiToken,
text: rocketMessage.msg,
channel: slackChannel.id,
username: rocketMessage.u && rocketMessage.u.username,
icon_url: iconUrl,
link_names: 1,
};
if (rocketMessage.tmid) {
const tmessage = Messages.findOneById(rocketMessage.tmid);
if (tmessage && tmessage.slackTs) {
data.thread_ts = tmessage.slackTs;
}
}
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 = this.slackAPI.sendMessage(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;
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 = this.slackAPI.updateMessage(data);
if (postResult) {
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 (rocketCh != null) {
this.addSlackChannel(rocketCh._id, slackMessage.channel);
}
}
processFileShare(slackMessage) {
if (!settings.get('SlackBridge_FileUpload_Enabled')) {
return;
}
const file = slackMessage.files[0];
if (file && file.url_private_download !== undefined) {
const rocketChannel = this.rocket.getChannel(slackMessage);
const rocketUser = this.rocket.getUser(slackMessage.user);
// Hack to notify that a file was attempted to be uploaded
delete slackMessage.subtype;
// If the text includes the file link, simply use the same text for the rocket message.
// If the link was not included, then use it instead of the message.
if (slackMessage.text.indexOf(file.permalink) < 0) {
slackMessage.text = file.permalink;
}
const ts = new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000);
const msgDataDefaults = {
_id: this.rocket.createRocketID(slackMessage.channel, slackMessage.ts),
ts,
updatedBySlack: true,
};
this.rocket.createAndSaveMessage(rocketChannel, rocketUser, slackMessage, msgDataDefaults, false);
}
}
/*
https://api.slack.com/events/message/message_deleted
*/
processMessageDeleted(slackMessage) {
if (slackMessage.previous_message) {
const rocketChannel = this.rocket.getChannel(slackMessage);
const rocketUser = Users.findOneById('rocket.cat', { fields: { username: 1 } });
if (rocketChannel && rocketUser) {
// Find the Rocket message to delete
let rocketMsgObj = 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 = Messages.findOneById(_id);
}
if (rocketMsgObj) {
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 = 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
};
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 = 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, this);
} catch (e) {
// http://www.mongodb.org/about/contributors/error-codes/
// 11000 == duplicate key error
if (e.name === 'MongoError' && e.code === 11000) {
return;
}
throw e;
}
}
}
processBotMessage(rocketChannel, slackMessage) {
const excludeBotNames = settings.get('SlackBridge_ExcludeBotnames');
if (slackMessage.username !== undefined && excludeBotNames && slackMessage.username.match(excludeBotNames)) {
return;
}
if (this.slackBotId) {
if (slackMessage.bot_id === this.slackBotId) {
return;
}
} else {
const slackChannel = this.getSlackChannel(rocketChannel._id);
if (this.isMessageBeingSent(slackMessage.username || slackMessage.bot_id, slackChannel.id)) {
return;
}
}
const rocketMsgObj = {
msg: 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) {
Messages.createUserJoinWithRoomIdAndUser(rocketChannel._id, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
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) {
Messages.createUserAddedWithRoomIdAndUser(rocketChannel._id, rocketUser, {
ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
u: {
_id: inviter._id,
username: inviter.username,
},
imported: 'slackbridge',
});
} else {
addUserToRoom(rocketChannel._id, rocketUser, inviter);
}
}
}
processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
Messages.createUserLeaveWithRoomIdAndUser(rocketChannel._id, rocketUser, {
ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000),
imported: 'slackbridge',
});
} else {
removeUserFromRoom(rocketChannel._id, rocketUser);
}
}
processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.topic, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
saveRoomTopic(rocketChannel._id, slackMessage.topic, rocketUser, false);
}
}
processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', rocketChannel._id, slackMessage.purpose, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
saveRoomTopic(rocketChannel._id, slackMessage.purpose, rocketUser, false);
}
}
processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
if (isImporting) {
Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rocketChannel._id, slackMessage.name, rocketUser, { ts: new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), imported: 'slackbridge' });
} else {
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: getUserAvatarURL(slackMessage.attachments[0].author_subname),
ts: new Date(parseInt(slackMessage.attachments[0].ts.split('.')[0]) * 1000),
}],
};
if (!isImporting) {
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;
}
logger.slack.error('Pinned item with no attachment');
}
processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting) {
switch (slackMessage.subtype) {
case 'bot_message':
return this.processBotMessage(rocketChannel, slackMessage);
case 'me_message':
return this.processMeMessage(rocketUser, slackMessage);
case 'channel_join':
return this.processChannelJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'group_join':
return this.processGroupJoinMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'channel_leave':
case 'group_leave':
return this.processLeaveMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'channel_topic':
case 'group_topic':
return this.processTopicMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'channel_purpose':
case 'group_purpose':
return this.processPurposeMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'channel_name':
case 'group_name':
return this.processNameMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'channel_archive':
case 'group_archive':
if (!isImporting) {
archiveRoom(rocketChannel);
}
return;
case 'channel_unarchive':
case 'group_unarchive':
if (!isImporting) {
unarchiveRoom(rocketChannel);
}
return;
case 'file_share':
return this.processShareMessage(rocketChannel, rocketUser, slackMessage, isImporting);
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':
return this.processPinnedItemMessage(rocketChannel, rocketUser, slackMessage, isImporting);
case 'unpinned_item':
logger.slack.error('Unpinned item not implemented');
}
}
/**
Uploads the file to the storage.
@param [Object] details an object with details about the upload. name, size, type, and rid
@param [String] fileUrl url of the file to download/import
@param [Object] user the Rocket.Chat user
@param [Object] room the Rocket.Chat room
@param [Date] timeStamp the timestamp the file was uploaded
**/
// details, slackMessage.file.url_private_download, rocketUser, rocketChannel, new Date(parseInt(slackMessage.ts.split('.')[0]) * 1000), isImporting);
uploadFileFromSlack(details, slackFileURL, rocketUser, rocketChannel, timeStamp, isImporting) {
const requestModule = /https/i.test(slackFileURL) ? https : http;
const parsedUrl = url.parse(slackFileURL, true);
parsedUrl.headers = { Authorization: `Bearer ${ this.apiToken }` };
requestModule.get(parsedUrl, Meteor.bindEnvironment((stream) => {
const fileStore = FileUpload.getStore('Uploads');
fileStore.insert(details, stream, (err, file) => {
if (err) {
throw new Error(err);
} else {
const url = file.url.replace(Meteor.absoluteUrl(), '/');
const attachment = {
title: file.name,
title_link: url,
};
if (/^image\/.+/.test(file.type)) {
attachment.image_url = url;
attachment.image_type = file.type;
attachment.image_size = file.size;
attachment.image_dimensions = file.identify && file.identify.size;
}
if (/^audio\/.+/.test(file.type)) {
attachment.audio_url = url;
attachment.audio_type = file.type;
attachment.audio_size = file.size;
}
if (/^video\/.+/.test(file.type)) {
attachment.video_url = url;
attachment.video_type = file.type;
attachment.video_size = file.size;
}
const msg = {
rid: details.rid,
ts: timeStamp,
msg: '',
file: {
_id: file._id,
},
groupable: false,
attachments: [attachment],
};
if (isImporting) {
msg.imported = 'slackbridge';
}
if (details.message_id && (typeof details.message_id === 'string')) {
msg._id = details.message_id;
}
return sendMessage(rocketUser, msg, rocketChannel, true);
}
});
}));
}
importFromHistory(family, options) {
logger.slack.debug('Importing messages history');
const data = this.slackAPI.getHistory(family, options);
if (Array.isArray(data.messages) && data.messages.length) {
let latest = 0;
for (const message of 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: data.has_more, ts: latest };
}
}
copyChannelInfo(rid, channelMap) {
logger.slack.debug('Copying users from Slack channel to Rocket.Chat', channelMap.id, rid);
const channel = this.slackAPI.getRoomInfo(channelMap.id);
if (channel) {
const members = this.slackAPI.getMembers(channelMap.id);
if (members && Array.isArray(members) && members.length) {
for (const member of members) {
const user = this.rocket.findUser(member) || this.rocket.addUser(member);
if (user) {
logger.slack.debug('Adding user to room', user.username, rid);
addUserToRoom(rid, user, null, true);
}
}
}
let topic = '';
let topic_last_set = 0;
let topic_creator = null;
if (channel && channel.topic && channel.topic.value) {
topic = channel.topic.value;
topic_last_set = channel.topic.last_set;
topic_creator = channel.topic.creator;
}
if (channel && channel.purpose && channel.purpose.value) {
if (topic_last_set) {
if (topic_last_set < channel.purpose.last_set) {
topic = channel.purpose.topic;
topic_creator = channel.purpose.creator;
}
} else {
topic = channel.purpose.topic;
topic_creator = channel.purpose.creator;
}
}
if (topic) {
const creator = this.rocket.findUser(topic_creator) || this.rocket.addUser(topic_creator);
logger.slack.debug('Setting room topic', rid, topic, creator.username);
saveRoomTopic(rid, topic, creator, false);
}
}
}
copyPins(rid, channelMap) {
const items = this.slackAPI.getPins(channelMap.id);
if (items && Array.isArray(items) && items.length) {
for (const pin of items) {
if (pin.message) {
const user = this.rocket.findUser(pin.message.user);
const msgObj = {
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: getUserAvatarURL(user.username),
ts: new Date(parseInt(pin.message.ts.split('.')[0]) * 1000),
}],
};
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 = 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();
}
const slack_room = this.postFindChannel(rocketchat_room.name);
if (slack_room) {
this.addSlackChannel(rid, slack_room.id);
return this.importMessages(rid, callback);
}
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'));
}
logger.slack.error('Could not find Rocket.Chat room with specified id', rid);
return callback(new Meteor.Error('error-invalid-room', 'Invalid room'));
}
}