snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
40
apps/meteor/server/bridges/slack/README.md
Normal file
40
apps/meteor/server/bridges/slack/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Rocket.Chat Slack Bridge
|
||||
|
||||
This package creates a bi-directional bridge between a single Slack installation and your Rocket.Chat installation.
|
||||
|
||||
* Configure Rocket.Chat with your Slack API token
|
||||
* Invite 'rocketbot' to your Slack channel
|
||||
|
||||
### Settings
|
||||
|
||||
The following can be configured in your Rocket.Chat Administration SlackBridge panel.
|
||||
|
||||
#### Enabled
|
||||
|
||||
#### API Token
|
||||
|
||||
#### Alias format
|
||||
|
||||
#### Exclude bots
|
||||
|
||||
#### SlackBridge Out Enabled
|
||||
|
||||
#### SlackBridge Out All
|
||||
|
||||
#### SlackBridge Out Channels
|
||||
|
||||
### Features
|
||||
|
||||
#### Group Chat Messages
|
||||
* Send and receive basic 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)
|
||||
|
||||
#### Files
|
||||
|
||||
#### Imports
|
||||
|
||||
### TODO
|
||||
* Support multiple slack installations
|
||||
* Private IM's
|
||||
551
apps/meteor/server/bridges/slack/RocketAdapter.ts
Normal file
551
apps/meteor/server/bridges/slack/RocketAdapter.ts
Normal file
@@ -0,0 +1,551 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import util from 'node:util';
|
||||
|
||||
import { Messages, Rooms, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { rocketLogger } from './logger';
|
||||
import { sleep } from '../../../lib/utils/sleep';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { sendMessage } from '../../lib/messages/sendMessage';
|
||||
import { createRoom } from '../../lib/rooms/createRoom';
|
||||
import { setUserAvatar } from '../../lib/users/setUserAvatar';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export default class RocketAdapter {
|
||||
constructor(slackBridge) {
|
||||
rocketLogger.debug('constructor');
|
||||
this.slackBridge = slackBridge;
|
||||
this.util = util;
|
||||
this.userTags = {};
|
||||
this.slackAdapters = [];
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.registerForEvents();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.unregisterForEvents();
|
||||
}
|
||||
|
||||
addSlack(slack) {
|
||||
if (this.slackAdapters.indexOf(slack) < 0) {
|
||||
this.slackAdapters.push(slack);
|
||||
}
|
||||
}
|
||||
|
||||
clearSlackAdapters() {
|
||||
this.slackAdapters = [];
|
||||
}
|
||||
|
||||
registerForEvents() {
|
||||
rocketLogger.debug('Register for events');
|
||||
callbacks.add('afterSaveMessage', this.onMessage.bind(this), callbacks.priority.LOW, 'SlackBridge_Out');
|
||||
callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), callbacks.priority.LOW, 'SlackBridge_Delete');
|
||||
callbacks.add('afterSetReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction');
|
||||
callbacks.add('afterUnsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction');
|
||||
}
|
||||
|
||||
unregisterForEvents() {
|
||||
rocketLogger.debug('Unregister for events');
|
||||
callbacks.remove('afterSaveMessage', 'SlackBridge_Out');
|
||||
callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete');
|
||||
callbacks.remove('afterSetReaction', 'SlackBridge_SetReaction');
|
||||
callbacks.remove('afterUnsetReaction', 'SlackBridge_UnSetReaction');
|
||||
}
|
||||
|
||||
async onMessageDelete(rocketMessageDeleted) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
try {
|
||||
if (!slack.getSlackChannel(rocketMessageDeleted.rid)) {
|
||||
// This is on a channel that the rocket bot is not subscribed on this slack server
|
||||
continue;
|
||||
}
|
||||
|
||||
rocketLogger.debug({ msg: 'onRocketMessageDelete', rocketMessageDeleted });
|
||||
await slack.postDeleteMessage(rocketMessageDeleted);
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onMessageDelete', err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onSetReaction(rocketMsg, { reaction }) {
|
||||
try {
|
||||
if (!this.slackBridge.isReactionsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
rocketLogger.debug('onRocketSetReaction');
|
||||
|
||||
if (rocketMsg._id && reaction) {
|
||||
if (this.slackBridge.reactionsMap.delete(`set${rocketMsg._id}${reaction}`)) {
|
||||
// This was a Slack reaction, we don't need to tell Slack about it
|
||||
return;
|
||||
}
|
||||
if (rocketMsg) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
const slackChannel = slack.getSlackChannel(rocketMsg.rid);
|
||||
if (slackChannel != null) {
|
||||
const slackTS = slack.getTimeStamp(rocketMsg);
|
||||
await slack.postReactionAdded(reaction.replace(/:/g, ''), slackChannel.id, slackTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onSetReaction', err });
|
||||
}
|
||||
}
|
||||
|
||||
async onUnSetReaction(rocketMsg, { reaction }) {
|
||||
try {
|
||||
if (!this.slackBridge.isReactionsEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
rocketLogger.debug('onRocketUnSetReaction');
|
||||
|
||||
if (rocketMsg._id && reaction) {
|
||||
if (this.slackBridge.reactionsMap.delete(`unset${rocketMsg._id}${reaction}`)) {
|
||||
// This was a Slack unset reaction, we don't need to tell Slack about it
|
||||
return;
|
||||
}
|
||||
|
||||
if (rocketMsg) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
const slackChannel = slack.getSlackChannel(rocketMsg.rid);
|
||||
if (slackChannel != null) {
|
||||
const slackTS = slack.getTimeStamp(rocketMsg);
|
||||
await slack.postReactionRemove(reaction.replace(/:/g, ''), slackChannel.id, slackTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onUnSetReaction', err });
|
||||
}
|
||||
}
|
||||
|
||||
async onMessage(rocketMessage) {
|
||||
for await (const slack of this.slackAdapters) {
|
||||
try {
|
||||
if (!slack.getSlackChannel(rocketMessage.rid)) {
|
||||
// This is on a channel that the rocket bot is not subscribed
|
||||
continue;
|
||||
}
|
||||
rocketLogger.debug({ msg: 'onRocketMessage', rocketMessage });
|
||||
|
||||
if (rocketMessage.editedAt) {
|
||||
// This is an Edit Event
|
||||
await this.processMessageChanged(rocketMessage, slack);
|
||||
continue;
|
||||
}
|
||||
// Ignore messages originating from Slack
|
||||
if (rocketMessage._id.indexOf('slack-') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rocketMessage.file) {
|
||||
await this.processFileShare(rocketMessage, slack);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A new message from Rocket.Chat
|
||||
await this.processSendMessage(rocketMessage, slack);
|
||||
} catch (err) {
|
||||
rocketLogger.error({ msg: 'Unhandled error onMessage', err });
|
||||
}
|
||||
}
|
||||
|
||||
return rocketMessage;
|
||||
}
|
||||
|
||||
async processSendMessage(rocketMessage, slack) {
|
||||
// Since we got this message, SlackBridge_Out_Enabled is true
|
||||
if (settings.get('SlackBridge_Out_All') === true) {
|
||||
await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
|
||||
} else {
|
||||
// They want to limit to certain groups
|
||||
const outSlackChannels = _.pluck(settings.get('SlackBridge_Out_Channels'), '_id') || [];
|
||||
// rocketLogger.debug('Out SlackChannels: ', outSlackChannels);
|
||||
if (outSlackChannels.indexOf(rocketMessage.rid) !== -1) {
|
||||
await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), rocketMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMessageAttachment(rocketMessage) {
|
||||
if (!rocketMessage.file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!rocketMessage.attachments || !rocketMessage.attachments.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileId = rocketMessage.file._id;
|
||||
return rocketMessage.attachments.find((attachment) => attachment.title_link && attachment.title_link.indexOf(`/${fileId}/`) >= 0);
|
||||
}
|
||||
|
||||
async processFileShare(rocketMessage, slack) {
|
||||
if (!settings.get('SlackBridge_FileUpload_Enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rocketMessage.file.name) {
|
||||
let fileName = rocketMessage.file.name;
|
||||
let text = rocketMessage.msg;
|
||||
|
||||
const attachment = this.getMessageAttachment(rocketMessage);
|
||||
if (attachment) {
|
||||
fileName = Meteor.absoluteUrl(attachment.title_link);
|
||||
if (!text) {
|
||||
text = attachment.description;
|
||||
}
|
||||
}
|
||||
|
||||
await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg: `${text} ${fileName}` });
|
||||
}
|
||||
}
|
||||
|
||||
async processMessageChanged(rocketMessage, slack) {
|
||||
if (rocketMessage) {
|
||||
if (rocketMessage.updatedBySlack) {
|
||||
// We have already processed this
|
||||
delete rocketMessage.updatedBySlack;
|
||||
return;
|
||||
}
|
||||
|
||||
// This was a change from Rocket.Chat
|
||||
const slackChannel = slack.getSlackChannel(rocketMessage.rid);
|
||||
await slack.postMessageUpdate(slackChannel, rocketMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async getChannel(slackMessage) {
|
||||
return slackMessage.channel ? (await this.findChannel(slackMessage.channel)) || this.addChannel(slackMessage.channel) : null;
|
||||
}
|
||||
|
||||
async getUser(slackUser) {
|
||||
return slackUser ? (await this.findUser(slackUser)) || this.addUser(slackUser) : null;
|
||||
}
|
||||
|
||||
createRocketID(slackChannel, ts) {
|
||||
return `slack-${slackChannel}-${ts.replace(/\./g, '-')}`;
|
||||
}
|
||||
|
||||
async findChannel(slackChannelId) {
|
||||
return Rooms.findOneByImportId(slackChannelId);
|
||||
}
|
||||
|
||||
async getRocketUsers(members, slackChannel) {
|
||||
const rocketUsers = [];
|
||||
for await (const member of members) {
|
||||
if (member !== slackChannel.creator) {
|
||||
const rocketUser = (await this.findUser(member)) || (await this.addUser(member));
|
||||
if (rocketUser && rocketUser.username) {
|
||||
rocketUsers.push(rocketUser.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rocketUsers;
|
||||
}
|
||||
|
||||
async getRocketUserCreator(slackChannel) {
|
||||
return slackChannel.creator ? (await this.findUser(slackChannel.creator)) || this.addUser(slackChannel.creator) : null;
|
||||
}
|
||||
|
||||
async addChannel(slackChannelID, hasRetried = false) {
|
||||
rocketLogger.debug({ msg: 'Adding Rocket.Chat channel from Slack', slackChannelID });
|
||||
let addedRoom;
|
||||
|
||||
for await (const slack of this.slackAdapters) {
|
||||
if (addedRoom) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const slackChannel = await slack.slackAPI.getRoomInfo(slackChannelID);
|
||||
if (slackChannel) {
|
||||
const members = await slack.slackAPI.getMembers(slackChannelID);
|
||||
if (!members) {
|
||||
rocketLogger.error('Could not fetch room members');
|
||||
continue;
|
||||
}
|
||||
|
||||
const rocketRoom = await Rooms.findOneByName(slackChannel.name);
|
||||
|
||||
if (rocketRoom || slackChannel.is_general) {
|
||||
slackChannel.rocketId = slackChannel.is_general ? 'GENERAL' : rocketRoom._id;
|
||||
await Rooms.addImportIds(slackChannel.rocketId, slackChannel.id);
|
||||
} else {
|
||||
const rocketUsers = await this.getRocketUsers(members, slackChannel);
|
||||
const rocketUserCreator = await this.getRocketUserCreator(slackChannel);
|
||||
|
||||
if (!rocketUserCreator) {
|
||||
rocketLogger.error({ msg: 'Could not fetch room creator information', creator: slackChannel.creator });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const isPrivate = slackChannel.is_private;
|
||||
const rocketChannel = await createRoom(isPrivate ? 'p' : 'c', slackChannel.name, rocketUserCreator, rocketUsers);
|
||||
slackChannel.rocketId = rocketChannel.rid;
|
||||
} catch (e) {
|
||||
if (!hasRetried) {
|
||||
rocketLogger.debug({ msg: 'Error adding channel from Slack. Will retry in 1s.', err: e });
|
||||
// If first time trying to create channel fails, could be because of multiple messages received at the same time. Try again once after 1s.
|
||||
await sleep(1000);
|
||||
return (await this.findChannel(slackChannelID)) || this.addChannel(slackChannelID, true);
|
||||
}
|
||||
rocketLogger.error({ msg: 'Error adding channel from Slack', err: e });
|
||||
}
|
||||
|
||||
const roomUpdate = {
|
||||
ts: new Date(slackChannel.created * 1000),
|
||||
};
|
||||
|
||||
let lastSetTopic = 0;
|
||||
if (slackChannel.topic && slackChannel.topic.value) {
|
||||
roomUpdate.topic = slackChannel.topic.value;
|
||||
lastSetTopic = slackChannel.topic.last_set;
|
||||
}
|
||||
|
||||
if (slackChannel.purpose && slackChannel.purpose.value && slackChannel.purpose.last_set > lastSetTopic) {
|
||||
roomUpdate.topic = slackChannel.purpose.value;
|
||||
}
|
||||
await Rooms.addImportIds(slackChannel.rocketId, slackChannel.id);
|
||||
slack.addSlackChannel(slackChannel.rocketId, slackChannelID);
|
||||
}
|
||||
|
||||
addedRoom = await Rooms.findOneById(slackChannel.rocketId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedRoom) {
|
||||
rocketLogger.debug('Channel not added');
|
||||
}
|
||||
return addedRoom;
|
||||
}
|
||||
|
||||
async findUser(slackUserID) {
|
||||
const rocketUser = await Users.findOneByImportId(slackUserID);
|
||||
if (rocketUser && !this.userTags[slackUserID]) {
|
||||
this.userTags[slackUserID] = {
|
||||
slack: `<@${slackUserID}>`,
|
||||
rocket: `@${rocketUser.username}`,
|
||||
};
|
||||
}
|
||||
return rocketUser;
|
||||
}
|
||||
|
||||
async addUser(slackUserID) {
|
||||
rocketLogger.debug({ msg: 'Adding Rocket.Chat user from Slack', slackUserID });
|
||||
let addedUser;
|
||||
for await (const slack of this.slackAdapters) {
|
||||
if (addedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await slack.slackAPI.getUser(slackUserID);
|
||||
if (user) {
|
||||
const rocketUserData = user;
|
||||
const isBot = rocketUserData.is_bot === true;
|
||||
const email = (rocketUserData.profile && rocketUserData.profile.email) || '';
|
||||
let existingRocketUser;
|
||||
if (!isBot) {
|
||||
existingRocketUser =
|
||||
(await Users.findOneByEmailAddress(email)) || (await Users.findOneByUsernameIgnoringCase(rocketUserData.name));
|
||||
} else {
|
||||
existingRocketUser = await Users.findOneByUsernameIgnoringCase(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 = await Accounts.createUserAsync(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'] = [];
|
||||
}
|
||||
|
||||
await Users.updateOne({ _id: rocketUserData.rocketId }, { $set: userUpdate });
|
||||
|
||||
const user = await 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 {
|
||||
await setUserAvatar(user, url, null, 'url');
|
||||
} catch (err) {
|
||||
rocketLogger.debug({ msg: 'Error setting user avatar from Slack', err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importIds = [rocketUserData.id];
|
||||
if (isBot && rocketUserData.profile && rocketUserData.profile.bot_id) {
|
||||
importIds.push(rocketUserData.profile.bot_id);
|
||||
}
|
||||
await Users.addImportIds(rocketUserData.rocketId, importIds);
|
||||
if (!this.userTags[slackUserID]) {
|
||||
this.userTags[slackUserID] = {
|
||||
slack: `<@${slackUserID}>`,
|
||||
rocket: `@${rocketUserData.name}`,
|
||||
};
|
||||
}
|
||||
addedUser = await Users.findOneById(rocketUserData.rocketId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addedUser) {
|
||||
rocketLogger.debug('User not added');
|
||||
}
|
||||
|
||||
return addedUser;
|
||||
}
|
||||
|
||||
addAliasToMsg(rocketUserName, rocketMsgObj) {
|
||||
const aliasFormat = settings.get('SlackBridge_AliasFormat');
|
||||
if (aliasFormat) {
|
||||
const alias = this.util.format(aliasFormat, rocketUserName);
|
||||
|
||||
if (alias !== rocketUserName) {
|
||||
rocketMsgObj.alias = alias;
|
||||
}
|
||||
}
|
||||
|
||||
return rocketMsgObj;
|
||||
}
|
||||
|
||||
async createAndSaveMessage(rocketChannel, rocketUser, slackMessage, rocketMsgDataDefaults, isImporting, slack) {
|
||||
if (slackMessage.type === 'message') {
|
||||
let rocketMsgObj = {};
|
||||
if (!_.isEmpty(slackMessage.subtype)) {
|
||||
rocketMsgObj = await slack.processSubtypedMessage(rocketChannel, rocketUser, slackMessage, isImporting);
|
||||
if (!rocketMsgObj) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
rocketMsgObj = {
|
||||
msg: await 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);
|
||||
}
|
||||
rocketMsgObj.slackTs = slackMessage.ts;
|
||||
if (slackMessage.thread_ts) {
|
||||
const tmessage = await Messages.findOneBySlackTs(slackMessage.thread_ts);
|
||||
if (tmessage) {
|
||||
rocketMsgObj.tmid = tmessage._id;
|
||||
}
|
||||
}
|
||||
|
||||
if (slackMessage.subtype === 'bot_message') {
|
||||
rocketUser = await Users.findOneById('rocket.cat', { projection: { 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') {
|
||||
setTimeout(async () => {
|
||||
if (slackMessage.bot_id && slackMessage.ts) {
|
||||
// Make sure that a message with the same bot_id and timestamp doesn't already exists
|
||||
const msg = await Messages.findOneBySlackBotIdAndSlackTs(slackMessage.bot_id, slackMessage.ts);
|
||||
if (!msg) {
|
||||
void sendMessage(rocketUser, rocketMsgObj, rocketChannel, { upsert: true });
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
rocketLogger.debug('Send message to Rocket.Chat');
|
||||
await sendMessage(rocketUser, rocketMsgObj, rocketChannel, { upsert: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async convertSlackMsgTxtToRocketTxtFormat(slackMsgTxt) {
|
||||
const regex = /(?:<@)([a-zA-Z0-9]+)(?:\|.+)?(?:>)/g;
|
||||
if (!_.isEmpty(slackMsgTxt)) {
|
||||
slackMsgTxt = slackMsgTxt.replace(/<!everyone>/g, '@all');
|
||||
slackMsgTxt = slackMsgTxt.replace(/<!channel>/g, '@all');
|
||||
slackMsgTxt = slackMsgTxt.replace(/<!here>/g, '@here');
|
||||
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');
|
||||
|
||||
const promises = [];
|
||||
|
||||
slackMsgTxt.replace(regex, async (match, userId) => {
|
||||
if (!this.userTags[userId]) {
|
||||
(await this.findUser(userId)) || (await this.addUser(userId)); // This adds userTags for the userId
|
||||
}
|
||||
const userTags = this.userTags[userId];
|
||||
if (userTags) {
|
||||
promises.push(slackMsgTxt.replace(userTags.slack, userTags.rocket));
|
||||
}
|
||||
});
|
||||
|
||||
const result = await Promise.all(promises);
|
||||
slackMsgTxt = slackMsgTxt.replace(regex, () => result.shift());
|
||||
} else {
|
||||
slackMsgTxt = '';
|
||||
}
|
||||
return slackMsgTxt;
|
||||
}
|
||||
}
|
||||
253
apps/meteor/server/bridges/slack/SlackAPI.ts
Normal file
253
apps/meteor/server/bridges/slack/SlackAPI.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
|
||||
|
||||
export class SlackAPI {
|
||||
constructor(apiOrBotToken) {
|
||||
this.token = apiOrBotToken;
|
||||
}
|
||||
|
||||
async getChannels(cursor = null) {
|
||||
let channels = [];
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/conversations.list', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
types: 'public_channel',
|
||||
exclude_archived: true,
|
||||
limit: 1000,
|
||||
...(cursor && { cursor }),
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
|
||||
if (response && response && Array.isArray(response.channels) && response.channels.length > 0) {
|
||||
channels = channels.concat(response.channels);
|
||||
if (response.response_metadata?.next_cursor) {
|
||||
const nextChannels = await this.getChannels(response.response_metadata.next_cursor);
|
||||
channels = channels.concat(nextChannels);
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
async getGroups(cursor = null) {
|
||||
let groups = [];
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/conversations.list', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
types: 'private_channel',
|
||||
exclude_archived: true,
|
||||
limit: 1000,
|
||||
...(cursor && { cursor }),
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
|
||||
if (response && response && Array.isArray(response.channels) && response.channels.length > 0) {
|
||||
groups = groups.concat(response.channels);
|
||||
if (response.response_metadata?.next_cursor) {
|
||||
const nextGroups = await this.getGroups(response.response_metadata.next_cursor);
|
||||
groups = groups.concat(nextGroups);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
async getRoomInfo(roomId) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch(`https://slack.com/api/conversations.info`, {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
channel: roomId,
|
||||
include_num_members: true,
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.channel;
|
||||
}
|
||||
|
||||
async getMembers(channelId) {
|
||||
const { num_members } = this.getRoomInfo(channelId);
|
||||
const MAX_MEMBERS_PER_CALL = 100;
|
||||
let members = [];
|
||||
let currentCursor = '';
|
||||
for (let index = 0; index < num_members; index += MAX_MEMBERS_PER_CALL) {
|
||||
const request = await fetch('https://slack.com/api/conversations.members', {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
channel: channelId,
|
||||
limit: MAX_MEMBERS_PER_CALL,
|
||||
...(currentCursor && { cursor: currentCursor }),
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
if (response && response && request.status === 200 && request.ok && Array.isArray(response.members)) {
|
||||
members = members.concat(response.members);
|
||||
const hasMoreItems = response.response_metadata?.next_cursor;
|
||||
if (hasMoreItems) {
|
||||
currentCursor = response.response_metadata.next_cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
async react(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/reactions.add', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async removeReaction(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/reactions.remove', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async removeMessage(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/chat.delete', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async sendMessage(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/chat.postMessage', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
return request.json();
|
||||
}
|
||||
|
||||
async updateMessage(data) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/chat.update', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
params: data,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && request.status === 200 && response && request.ok;
|
||||
}
|
||||
|
||||
async getHistory(options) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch(`https://slack.com/api/conversations.history`, {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: options,
|
||||
});
|
||||
const response = await request.json();
|
||||
return response;
|
||||
}
|
||||
|
||||
async getPins(channelId) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/pins.list', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
channel: channelId,
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.items;
|
||||
}
|
||||
|
||||
async getUser(userId) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/users.info', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
},
|
||||
params: {
|
||||
user: userId,
|
||||
},
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.user;
|
||||
}
|
||||
|
||||
static async verifyToken(token) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/auth.test', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const response = await request.json();
|
||||
return response && response && request.status === 200 && request.ok && response.ok;
|
||||
}
|
||||
|
||||
static async verifyAppCredentials({ botToken, appToken }) {
|
||||
// SECURITY: the URL is a default hardcoded value or an envvar/setting set by an admin. It's safe to disable this check.
|
||||
const request = await fetch('https://slack.com/api/apps.connections.open', {
|
||||
ignoreSsrfValidation: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${appToken}`,
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
const response = await request.json();
|
||||
const isAppTokenOk = response && response && request.status === 200 && request.ok && response.ok;
|
||||
const isBotTokenOk = await this.verifyToken(botToken);
|
||||
return isAppTokenOk && isBotTokenOk;
|
||||
}
|
||||
}
|
||||
1453
apps/meteor/server/bridges/slack/SlackAdapter.ts
Normal file
1453
apps/meteor/server/bridges/slack/SlackAdapter.ts
Normal file
File diff suppressed because it is too large
Load Diff
3
apps/meteor/server/bridges/slack/index.ts
Normal file
3
apps/meteor/server/bridges/slack/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import './slackbridge';
|
||||
import './slackbridge_import.server';
|
||||
import './removeChannelLinks';
|
||||
8
apps/meteor/server/bridges/slack/logger.ts
Normal file
8
apps/meteor/server/bridges/slack/logger.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
|
||||
const logger = new Logger('SlackBridge');
|
||||
|
||||
export const connLogger = logger.section('Connection');
|
||||
export const classLogger = logger.section('Class');
|
||||
export const slackLogger = logger.section('Slack');
|
||||
export const rocketLogger = logger.section('Rocket');
|
||||
41
apps/meteor/server/bridges/slack/removeChannelLinks.ts
Normal file
41
apps/meteor/server/bridges/slack/removeChannelLinks.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
removeSlackBridgeChannelLinks(): { message: string; params: unknown[] };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async removeSlackBridgeChannelLinks() {
|
||||
const user = await Meteor.userAsync();
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'removeSlackBridgeChannelLinks',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(user, 'remove-slackbridge-links'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'removeSlackBridgeChannelLinks',
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.get('SlackBridge_Enabled') !== true) {
|
||||
throw new Meteor.Error('SlackBridge_disabled');
|
||||
}
|
||||
|
||||
await Rooms.unsetAllImportIds();
|
||||
|
||||
return {
|
||||
message: 'Slackbridge_channel_links_removed_successfully',
|
||||
params: [],
|
||||
};
|
||||
},
|
||||
});
|
||||
199
apps/meteor/server/bridges/slack/slackbridge.ts
Normal file
199
apps/meteor/server/bridges/slack/slackbridge.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import RocketAdapter from './RocketAdapter';
|
||||
import SlackAdapter from './SlackAdapter';
|
||||
import { classLogger, connLogger } from './logger';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
/**
|
||||
* SlackBridge interfaces between this Rocket installation and a remote Slack installation.
|
||||
*/
|
||||
class SlackBridgeClass {
|
||||
constructor() {
|
||||
this.isEnabled = false;
|
||||
this.isLegacyRTM = true;
|
||||
this.slackAdapters = [];
|
||||
this.rocket = new RocketAdapter(this);
|
||||
this.reactionsMap = new Map(); // Sync object between rocket and slack
|
||||
|
||||
this.connected = false;
|
||||
this.rocket.clearSlackAdapters();
|
||||
|
||||
// Settings that we cache versus looking up at runtime
|
||||
this.apiTokens = false;
|
||||
this.botTokens = false;
|
||||
this.appTokens = false;
|
||||
this.signingSecrets = false;
|
||||
this.aliasFormat = '';
|
||||
this.excludeBotnames = '';
|
||||
this.isReactionsEnabled = true;
|
||||
|
||||
this.processSettings();
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.connected === false) {
|
||||
this.slackAdapters = [];
|
||||
this.rocket.clearSlackAdapters();
|
||||
|
||||
if (this.isLegacyRTM) {
|
||||
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 }).catch((err) => connLogger.error({ msg: 'error connecting to slack', err }));
|
||||
});
|
||||
} else {
|
||||
const botTokenList = this.botTokens.split('\n'); // Bot token list
|
||||
const appTokenList = this.appTokens.split('\n'); // App token list
|
||||
const signingSecretList = this.signingSecrets.split('\n'); // Signing secret list
|
||||
|
||||
// Check if the number of tokens are the same
|
||||
if (botTokenList.length !== appTokenList.length || botTokenList.length !== signingSecretList.length) {
|
||||
connLogger.error('error connecting to slack: number of tokens are not the same');
|
||||
return;
|
||||
}
|
||||
|
||||
const appCredentials = botTokenList.map((botToken, i) => ({
|
||||
botToken,
|
||||
appToken: appTokenList[i],
|
||||
signingSecret: signingSecretList[i],
|
||||
}));
|
||||
|
||||
appCredentials.forEach((appCredential) => {
|
||||
const slack = new SlackAdapter(this);
|
||||
slack.setRocket(this.rocket);
|
||||
this.rocket.addSlack(slack);
|
||||
this.slackAdapters.push(slack);
|
||||
|
||||
slack.connect({ appCredential }).catch((err) => connLogger.error({ msg: 'error connecting to slack', err }));
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.get('SlackBridge_Out_Enabled')) {
|
||||
this.rocket.connect();
|
||||
}
|
||||
|
||||
this.connected = true;
|
||||
connLogger.info('Enabled');
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
await this.disconnect();
|
||||
// connect if either apiTokens or appCredentials are set
|
||||
if (this.isLegacyRTM && this.apiTokens) {
|
||||
this.connect();
|
||||
} else if (!this.isLegacyRTM && this.botTokens && this.appTokens && this.signingSecrets) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
debouncedReconnectIfEnabled = debounce(() => {
|
||||
if (this.isEnabled) {
|
||||
this.reconnect();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
async disconnect() {
|
||||
try {
|
||||
if (this.connected === true) {
|
||||
await this.rocket.disconnect();
|
||||
await Promise.all(this.slackAdapters.map((slack) => slack.disconnect()));
|
||||
this.slackAdapters = [];
|
||||
this.connected = false;
|
||||
connLogger.info('Slack Bridge Disconnected');
|
||||
}
|
||||
} catch (error) {
|
||||
connLogger.error({ msg: 'An error occurred during disconnection', err: error });
|
||||
}
|
||||
}
|
||||
|
||||
processSettings() {
|
||||
// Check if legacy realtime api is enabled
|
||||
settings.watch('SlackBridge_UseLegacy', (value) => {
|
||||
if (value !== this.isLegacyRTM) {
|
||||
this.isLegacyRTM = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_UseLegacy', value });
|
||||
});
|
||||
|
||||
// Slack installtion Bot token
|
||||
settings.watch('SlackBridge_BotToken', (value) => {
|
||||
if (value !== this.botTokens) {
|
||||
this.botTokens = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_BotToken', value });
|
||||
});
|
||||
// Slack installtion App token
|
||||
settings.watch('SlackBridge_AppToken', (value) => {
|
||||
if (value !== this.appTokens) {
|
||||
this.appTokens = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_AppToken', value });
|
||||
});
|
||||
// Slack installtion Signing token
|
||||
settings.watch('SlackBridge_SigningSecret', (value) => {
|
||||
if (value !== this.signingSecrets) {
|
||||
this.signingSecrets = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_SigningSecret', value });
|
||||
});
|
||||
|
||||
// Slack installation API token
|
||||
settings.watch('SlackBridge_APIToken', (value) => {
|
||||
if (value !== this.apiTokens) {
|
||||
this.apiTokens = value;
|
||||
this.debouncedReconnectIfEnabled();
|
||||
}
|
||||
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_APIToken', value });
|
||||
});
|
||||
|
||||
// Import messages from Slack with an alias; %s is replaced by the username of the user. If empty, no alias will be used.
|
||||
settings.watch('SlackBridge_AliasFormat', (value) => {
|
||||
this.aliasFormat = value;
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_AliasFormat', value });
|
||||
});
|
||||
|
||||
// Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.
|
||||
settings.watch('SlackBridge_ExcludeBotnames', (value) => {
|
||||
this.excludeBotnames = value;
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_ExcludeBotnames', value });
|
||||
});
|
||||
|
||||
// Reactions
|
||||
settings.watch('SlackBridge_Reactions_Enabled', (value) => {
|
||||
this.isReactionsEnabled = value;
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_Reactions_Enabled', value });
|
||||
});
|
||||
|
||||
// Is this entire SlackBridge enabled
|
||||
settings.watch('SlackBridge_Enabled', (value) => {
|
||||
if (this.isEnabled !== value) {
|
||||
this.isEnabled = value;
|
||||
if (this.isEnabled) {
|
||||
this.debouncedReconnectIfEnabled();
|
||||
} else {
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
classLogger.debug({ msg: 'Setting: SlackBridge_Enabled', value });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const SlackBridge = new SlackBridgeClass();
|
||||
@@ -0,0 +1,84 @@
|
||||
// This is a JS File that was renamed to TS so it won't lose its git history when converted to TS
|
||||
// TODO: Remove the following lint/ts instructions when the file gets properly converted
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
// @ts-nocheck
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { Match } from 'meteor/check';
|
||||
|
||||
import { SlackBridge } from './slackbridge';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { msgStream } from '../../lib/messaging/msgStream';
|
||||
import { slashCommands } from '../../lib/utils/slashCommand';
|
||||
|
||||
async function SlackBridgeImport({ command, params, message, userId }) {
|
||||
if (command !== 'slackbridge-import' || !Match.test(params, String)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(message.rid);
|
||||
const channel = room.name;
|
||||
const user = await Users.findOneById(userId);
|
||||
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_start', {
|
||||
username: user.username,
|
||||
channelName: channel,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
for await (const slack of SlackBridge.slackAdapters) {
|
||||
await slack.importMessages(message.rid, (error) => {
|
||||
if (error) {
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_error', {
|
||||
channelName: channel,
|
||||
errorMessage: error.message,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_finish', {
|
||||
channelName: channel,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
msgStream.emit(message.rid, {
|
||||
_id: Random.id(),
|
||||
rid: message.rid,
|
||||
u: { username: 'rocket.cat' },
|
||||
ts: new Date(),
|
||||
msg: i18n.t('SlackBridge_error', {
|
||||
channelName: channel,
|
||||
errorMessage: error.message,
|
||||
interpolation: { escapeValue: false },
|
||||
lng: user.language,
|
||||
}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
slashCommands.add({ command: 'slackbridge-import', callback: SlackBridgeImport });
|
||||
Reference in New Issue
Block a user