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:
730
apps/meteor/server/lib/import/slack/SlackImporter.ts
Normal file
730
apps/meteor/server/lib/import/slack/SlackImporter.ts
Normal file
@@ -0,0 +1,730 @@
|
||||
import type { IImportUser, IImportMessage, IImportPendingFile } from '@rocket.chat/core-typings';
|
||||
import { Messages, Settings, ImportData } from '@rocket.chat/models';
|
||||
import type { IZipEntry } from 'adm-zip';
|
||||
|
||||
import { Importer, ProgressStep, ImporterWebsocket } from '..';
|
||||
import { MentionsParser } from '../../../../app/mentions/lib/MentionsParser';
|
||||
import { settings } from '../../../settings';
|
||||
import { notifyOnSettingChanged } from '../../notifyListener';
|
||||
import { getUserAvatarURL } from '../../utils/getUserAvatarURL';
|
||||
import type { ImporterProgress } from '../classes/ImporterProgress';
|
||||
|
||||
type SlackChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
topic?: {
|
||||
value: string;
|
||||
creator: string;
|
||||
last_set: number;
|
||||
};
|
||||
members: string[];
|
||||
purpose?: {
|
||||
value: string;
|
||||
creator: string;
|
||||
last_set: number;
|
||||
};
|
||||
created: number;
|
||||
creator: string | null;
|
||||
is_general: boolean;
|
||||
is_archived: boolean;
|
||||
};
|
||||
|
||||
type SlackUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
profile: {
|
||||
real_name: string;
|
||||
email: string;
|
||||
image_512: string;
|
||||
image_original: string;
|
||||
status_text: string;
|
||||
title: string;
|
||||
};
|
||||
tz_offset: number;
|
||||
deleted: boolean;
|
||||
is_bot: boolean;
|
||||
};
|
||||
|
||||
type SlackFile = {
|
||||
id: string;
|
||||
url_private_download: string;
|
||||
size: number;
|
||||
name: string;
|
||||
is_external: boolean;
|
||||
};
|
||||
|
||||
type SlackMessage = {
|
||||
id: string;
|
||||
ts: string;
|
||||
user: string;
|
||||
reactions?: {
|
||||
name: string;
|
||||
users: string[];
|
||||
}[];
|
||||
type: 'message';
|
||||
subtype?: string;
|
||||
files?: SlackFile[];
|
||||
text: string;
|
||||
edited?: {
|
||||
ts: string;
|
||||
user: string;
|
||||
};
|
||||
thread_ts?: string;
|
||||
reply_users?: string[];
|
||||
reply_count?: number;
|
||||
replies?: {
|
||||
user: string;
|
||||
}[];
|
||||
latest_reply: string;
|
||||
icons?: {
|
||||
emoji: string;
|
||||
};
|
||||
attachments?: SlackAttachment[];
|
||||
} & (
|
||||
| {
|
||||
subtype: 'channel_purpose' | 'group_purpose';
|
||||
purpose: string;
|
||||
}
|
||||
| {
|
||||
subtype: 'channel_join' | 'group_join' | 'channel_leave' | 'group_leave';
|
||||
}
|
||||
| {
|
||||
subtype: 'channel_topic' | 'group_topic';
|
||||
topic: string;
|
||||
}
|
||||
| {
|
||||
subtype: 'channel_name' | 'group_name';
|
||||
name: string;
|
||||
}
|
||||
| {
|
||||
subtype: 'pinned_item';
|
||||
attachments: SlackAttachment[];
|
||||
}
|
||||
| {
|
||||
subtype: 'file_share';
|
||||
file: SlackFile;
|
||||
}
|
||||
| {
|
||||
subtype: 'me_message';
|
||||
}
|
||||
);
|
||||
|
||||
type SlackAttachment = {
|
||||
text: string;
|
||||
title: string;
|
||||
fallback: string;
|
||||
author_subname: string;
|
||||
};
|
||||
|
||||
export class SlackImporter extends Importer {
|
||||
private _useUpsert = false;
|
||||
|
||||
async prepareChannelsFile(entry: IZipEntry): Promise<number> {
|
||||
await super.updateProgress(ProgressStep.PREPARING_CHANNELS);
|
||||
const data = (JSON.parse(entry.getData().toString()) as SlackChannel[]).filter(
|
||||
(channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null,
|
||||
);
|
||||
|
||||
this.logger.debug({ msg: 'loaded channels', count: data.length });
|
||||
|
||||
await this.addCountToTotal(data.length);
|
||||
|
||||
for (const channel of data) {
|
||||
await this.converter.addChannel({
|
||||
_id: channel.is_general ? 'general' : undefined,
|
||||
u: {
|
||||
_id: this._replaceSlackUserId(channel.creator),
|
||||
},
|
||||
importIds: [channel.id],
|
||||
name: channel.name,
|
||||
users: this._replaceSlackUserIds(channel.members),
|
||||
t: 'c',
|
||||
topic: channel.topic?.value || undefined,
|
||||
description: channel.purpose?.value || undefined,
|
||||
ts: channel.created ? new Date(channel.created * 1000) : undefined,
|
||||
archived: channel.is_archived,
|
||||
});
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
async prepareGroupsFile(entry: IZipEntry): Promise<number> {
|
||||
await super.updateProgress(ProgressStep.PREPARING_CHANNELS);
|
||||
const data = (JSON.parse(entry.getData().toString()) as SlackChannel[]).filter(
|
||||
(channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null,
|
||||
);
|
||||
|
||||
this.logger.debug({ msg: 'loaded groups', count: data.length });
|
||||
|
||||
await this.addCountToTotal(data.length);
|
||||
|
||||
for (const channel of data) {
|
||||
await this.converter.addChannel({
|
||||
u: {
|
||||
_id: this._replaceSlackUserId(channel.creator),
|
||||
},
|
||||
importIds: [channel.id],
|
||||
name: channel.name,
|
||||
users: this._replaceSlackUserIds(channel.members),
|
||||
t: 'p',
|
||||
topic: channel.topic?.value || undefined,
|
||||
description: channel.purpose?.value || undefined,
|
||||
ts: channel.created ? new Date(channel.created * 1000) : undefined,
|
||||
archived: channel.is_archived,
|
||||
});
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
async prepareMpimpsFile(entry: IZipEntry): Promise<number> {
|
||||
await super.updateProgress(ProgressStep.PREPARING_CHANNELS);
|
||||
const data = (JSON.parse(entry.getData().toString()) as SlackChannel[]).filter(
|
||||
(channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null,
|
||||
);
|
||||
|
||||
this.logger.debug({ msg: 'loaded mpims', count: data.length });
|
||||
|
||||
await this.addCountToTotal(data.length);
|
||||
|
||||
const maxUsers = settings.get<number>('DirectMesssage_maxUsers') || 1;
|
||||
|
||||
for (const channel of data) {
|
||||
await this.converter.addChannel({
|
||||
u: {
|
||||
_id: this._replaceSlackUserId(channel.creator),
|
||||
},
|
||||
importIds: [channel.id],
|
||||
name: channel.name,
|
||||
users: this._replaceSlackUserIds(channel.members),
|
||||
t: channel.members.length > maxUsers ? 'p' : 'd',
|
||||
topic: channel.topic?.value || undefined,
|
||||
description: channel.purpose?.value || undefined,
|
||||
ts: channel.created ? new Date(channel.created * 1000) : undefined,
|
||||
archived: channel.is_archived,
|
||||
});
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
async prepareDMsFile(entry: IZipEntry): Promise<number> {
|
||||
await super.updateProgress(ProgressStep.PREPARING_CHANNELS);
|
||||
const data = JSON.parse(entry.getData().toString()) as SlackChannel[];
|
||||
|
||||
this.logger.debug({ msg: 'loaded dms', count: data.length });
|
||||
|
||||
await this.addCountToTotal(data.length);
|
||||
for (const channel of data) {
|
||||
await this.converter.addChannel({
|
||||
importIds: [channel.id],
|
||||
users: this._replaceSlackUserIds(channel.members),
|
||||
t: 'd',
|
||||
ts: channel.created ? new Date(channel.created * 1000) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
async prepareUsersFile(entry: IZipEntry): Promise<number> {
|
||||
await super.updateProgress(ProgressStep.PREPARING_USERS);
|
||||
const data = JSON.parse(entry.getData().toString()) as SlackUser[];
|
||||
|
||||
this.logger.debug({ msg: 'loaded users', count: data.length });
|
||||
|
||||
// Insert the users record
|
||||
await this.updateRecord({ 'count.users': data.length });
|
||||
await this.addCountToTotal(data.length);
|
||||
|
||||
for (const user of data) {
|
||||
const newUser: IImportUser = {
|
||||
emails: [],
|
||||
importIds: [user.id],
|
||||
username: user.name,
|
||||
name: user.profile.real_name,
|
||||
utcOffset: user.tz_offset && user.tz_offset / 3600,
|
||||
avatarUrl: user.profile.image_original || user.profile.image_512,
|
||||
deleted: user.deleted,
|
||||
statusText: user.profile.status_text || undefined,
|
||||
bio: user.profile.title || undefined,
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
if (user.profile.email) {
|
||||
newUser.emails.push(user.profile.email);
|
||||
}
|
||||
|
||||
if (user.is_bot) {
|
||||
newUser.roles = ['bot'];
|
||||
newUser.type = 'bot';
|
||||
}
|
||||
|
||||
await this.converter.addUser(newUser);
|
||||
}
|
||||
|
||||
return data.length;
|
||||
}
|
||||
|
||||
override async prepareUsingLocalFile(fullFilePath: string): Promise<ImporterProgress> {
|
||||
this.logger.debug('start preparing import operation');
|
||||
await this.converter.clearImportData();
|
||||
|
||||
const zip = new this.AdmZip(fullFilePath);
|
||||
const totalEntries = zip.getEntryCount();
|
||||
|
||||
let userCount = 0;
|
||||
let messagesCount = 0;
|
||||
let channelCount = 0;
|
||||
let count = 0;
|
||||
|
||||
ImporterWebsocket.progressUpdated({ rate: 0 });
|
||||
let oldRate = 0;
|
||||
|
||||
const increaseProgress = () => {
|
||||
try {
|
||||
count++;
|
||||
const rate = Math.floor((count * 1000) / totalEntries) / 10;
|
||||
if (rate > oldRate) {
|
||||
ImporterWebsocket.progressUpdated({ rate });
|
||||
oldRate = rate;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({ msg: 'Error updating progress', err });
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// we need to iterate the zip file twice so that all channels are loaded before the messages
|
||||
for (const entry of zip.getEntries()) {
|
||||
try {
|
||||
if (entry.entryName === 'channels.json') {
|
||||
channelCount += await this.prepareChannelsFile(entry);
|
||||
await this.updateRecord({ 'count.channels': channelCount });
|
||||
increaseProgress();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.entryName === 'groups.json') {
|
||||
channelCount += await this.prepareGroupsFile(entry);
|
||||
await this.updateRecord({ 'count.channels': channelCount });
|
||||
increaseProgress();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.entryName === 'mpims.json') {
|
||||
channelCount += await this.prepareMpimpsFile(entry);
|
||||
await this.updateRecord({ 'count.channels': channelCount });
|
||||
increaseProgress();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.entryName === 'dms.json') {
|
||||
channelCount += await this.prepareDMsFile(entry);
|
||||
await this.updateRecord({ 'count.channels': channelCount });
|
||||
increaseProgress();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.entryName === 'users.json') {
|
||||
userCount = await this.prepareUsersFile(entry);
|
||||
increaseProgress();
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({ msg: 'Error adding missed type', err });
|
||||
}
|
||||
}
|
||||
|
||||
if (userCount) {
|
||||
const value = await Settings.incrementValueById('Slack_Importer_Count', userCount, { returnDocument: 'after' });
|
||||
if (value) {
|
||||
void notifyOnSettingChanged(value);
|
||||
}
|
||||
}
|
||||
|
||||
const missedTypes: Record<string, SlackMessage> = {};
|
||||
// If we have no slack message yet, then we can insert them instead of upserting
|
||||
this._useUpsert = !(await Messages.findOne({ _id: /slack\-.*/ }));
|
||||
|
||||
for (const entry of zip.getEntries()) {
|
||||
try {
|
||||
if (entry.entryName.includes('__MACOSX') || entry.entryName.includes('.DS_Store')) {
|
||||
count++;
|
||||
this.logger.debug({ msg: 'Ignoring the file', entryName: entry.entryName });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (['channels.json', 'groups.json', 'mpims.json', 'dms.json', 'users.json'].includes(entry.entryName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isDirectory && entry.entryName.includes('/')) {
|
||||
const item = entry.entryName.split('/');
|
||||
|
||||
const channel = item[0];
|
||||
const date = item[1].split('.')[0];
|
||||
|
||||
try {
|
||||
// Insert the messages records
|
||||
if (this.progress.step !== ProgressStep.PREPARING_MESSAGES) {
|
||||
await super.updateProgress(ProgressStep.PREPARING_MESSAGES);
|
||||
}
|
||||
|
||||
const tempMessages = JSON.parse(entry.getData().toString()) as SlackMessage[];
|
||||
messagesCount += tempMessages.length;
|
||||
await this.updateRecord({ messagesstatus: `${channel}/${date}` });
|
||||
await this.addCountToTotal(tempMessages.length);
|
||||
|
||||
const slackChannelId = await ImportData.findChannelImportIdByNameOrImportId(channel);
|
||||
|
||||
if (slackChannelId) {
|
||||
for (const message of tempMessages) {
|
||||
await this.prepareMessageObject(message, missedTypes, slackChannelId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn({ msg: 'Entry is not a valid JSON file; unable to import', entryName: entry.entryName, err: error });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({ msg: 'Error processing message entry', err });
|
||||
}
|
||||
|
||||
increaseProgress();
|
||||
}
|
||||
|
||||
if (Object.keys(missedTypes).length > 0) {
|
||||
this.logger.info({ msg: 'Missed import types', missedTypes });
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({ msg: 'Error preparing import using local file', err });
|
||||
throw err;
|
||||
}
|
||||
|
||||
ImporterWebsocket.progressUpdated({ rate: 100 });
|
||||
await this.updateRecord({ 'count.messages': messagesCount, 'messagesstatus': null });
|
||||
|
||||
return this.progress;
|
||||
}
|
||||
|
||||
parseMentions(newMessage: IImportMessage): void {
|
||||
const mentionsParser = new MentionsParser({
|
||||
pattern: () => '[0-9a-zA-Z]+',
|
||||
useRealName: () => settings.get<boolean>('UI_Use_Real_Name'),
|
||||
me: () => 'me',
|
||||
});
|
||||
|
||||
const users = mentionsParser
|
||||
.getUserMentions(newMessage.msg)
|
||||
.filter((u: string) => u)
|
||||
.map((uid: string) => this._replaceSlackUserId(uid.slice(1, uid.length)));
|
||||
if (users.length) {
|
||||
if (!newMessage.mentions) {
|
||||
newMessage.mentions = [];
|
||||
}
|
||||
newMessage.mentions.push(...users);
|
||||
}
|
||||
|
||||
const channels = mentionsParser
|
||||
.getChannelMentions(newMessage.msg)
|
||||
.filter((c: string) => c)
|
||||
.map((name: string) => name.slice(1, name.length));
|
||||
if (channels.length) {
|
||||
if (!newMessage.channels) {
|
||||
newMessage.channels = [];
|
||||
}
|
||||
newMessage.channels.push(...channels);
|
||||
}
|
||||
}
|
||||
|
||||
async processMessageSubType(
|
||||
message: SlackMessage,
|
||||
slackChannelId: string,
|
||||
newMessage: IImportMessage,
|
||||
missedTypes: Record<string, SlackMessage>,
|
||||
): Promise<boolean> {
|
||||
const ignoreTypes: Record<string, boolean> = { bot_add: true, file_comment: true, file_mention: true };
|
||||
|
||||
switch (message.subtype) {
|
||||
case 'channel_join':
|
||||
case 'group_join':
|
||||
newMessage.t = 'uj';
|
||||
newMessage.groupable = false;
|
||||
return true;
|
||||
case 'channel_leave':
|
||||
case 'group_leave':
|
||||
newMessage.t = 'ul';
|
||||
newMessage.groupable = false;
|
||||
return true;
|
||||
case 'channel_purpose':
|
||||
case 'group_purpose':
|
||||
newMessage.t = 'room_changed_description';
|
||||
newMessage.groupable = false;
|
||||
newMessage.msg = message.purpose;
|
||||
return true;
|
||||
case 'channel_topic':
|
||||
case 'group_topic':
|
||||
newMessage.t = 'room_changed_topic';
|
||||
newMessage.groupable = false;
|
||||
newMessage.msg = message.topic;
|
||||
return true;
|
||||
case 'channel_name':
|
||||
case 'group_name':
|
||||
newMessage.t = 'r';
|
||||
newMessage.msg = message.name;
|
||||
newMessage.groupable = false;
|
||||
return true;
|
||||
case 'pinned_item':
|
||||
if (message.attachments) {
|
||||
if (!newMessage.attachments) {
|
||||
newMessage.attachments = [];
|
||||
}
|
||||
newMessage.attachments.push({
|
||||
text: this.convertSlackMessageToRocketChat(message.attachments[0].text),
|
||||
author_name: message.attachments[0].author_subname,
|
||||
author_icon: getUserAvatarURL(message.attachments[0].author_subname),
|
||||
});
|
||||
newMessage.t = 'message_pinned';
|
||||
}
|
||||
break;
|
||||
case 'file_share':
|
||||
if (message.file?.url_private_download) {
|
||||
const fileId = this.makeSlackMessageId(slackChannelId, message.ts, 'share');
|
||||
const fileMessage: IImportMessage = {
|
||||
_id: fileId,
|
||||
rid: newMessage.rid,
|
||||
ts: newMessage.ts,
|
||||
msg: '',
|
||||
_hidden: true,
|
||||
_importFile: this.convertSlackFileToPendingFile(message.file),
|
||||
u: {
|
||||
_id: newMessage.u._id,
|
||||
},
|
||||
};
|
||||
|
||||
if (message.thread_ts && message.thread_ts !== message.ts) {
|
||||
fileMessage.tmid = this.makeSlackMessageId(slackChannelId, message.thread_ts);
|
||||
}
|
||||
|
||||
await this.converter.addMessage(fileMessage, this._useUpsert);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (!missedTypes[message.subtype] && !ignoreTypes[message.subtype]) {
|
||||
missedTypes[message.subtype] = message;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
makeSlackMessageId(channelId: string, ts: string, fileIndex?: string): string {
|
||||
const base = `slack-${channelId}-${ts.replace(/\./g, '-')}`;
|
||||
|
||||
if (fileIndex) {
|
||||
return `${base}-file${fileIndex}`;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
async prepareMessageObject(message: SlackMessage, missedTypes: Record<string, SlackMessage>, slackChannelId: string): Promise<void> {
|
||||
const id = this.makeSlackMessageId(slackChannelId, message.ts);
|
||||
const newMessage: IImportMessage = {
|
||||
_id: id,
|
||||
rid: slackChannelId,
|
||||
ts: new Date(parseInt(message.ts.split('.')[0]) * 1000),
|
||||
msg: '',
|
||||
u: {
|
||||
_id: this._replaceSlackUserId(message.user),
|
||||
},
|
||||
};
|
||||
|
||||
// Process the reactions
|
||||
if (message.reactions && message.reactions.length > 0) {
|
||||
newMessage.reactions = message.reactions.reduce(
|
||||
(newReactions, reaction) => {
|
||||
const name = `:${reaction.name}:`;
|
||||
return {
|
||||
...newReactions,
|
||||
...(reaction.users?.length ? { name: { name, users: this._replaceSlackUserIds(reaction.users) } } : {}),
|
||||
};
|
||||
},
|
||||
{} as Required<IImportMessage>['reactions'],
|
||||
);
|
||||
}
|
||||
|
||||
if (message.type === 'message') {
|
||||
if (message.files) {
|
||||
let fileIndex = 0;
|
||||
const promises = message.files.map(async (file) => {
|
||||
fileIndex++;
|
||||
|
||||
const fileId = this.makeSlackMessageId(slackChannelId, message.ts, String(fileIndex));
|
||||
const fileMessage: IImportMessage = {
|
||||
_id: fileId,
|
||||
rid: slackChannelId,
|
||||
ts: newMessage.ts,
|
||||
msg: '',
|
||||
_hidden: true,
|
||||
_importFile: this.convertSlackFileToPendingFile(file),
|
||||
u: {
|
||||
_id: this._replaceSlackUserId(message.user),
|
||||
},
|
||||
};
|
||||
|
||||
if (message.thread_ts && message.thread_ts !== message.ts) {
|
||||
fileMessage.tmid = this.makeSlackMessageId(slackChannelId, message.thread_ts);
|
||||
}
|
||||
|
||||
await this.converter.addMessage(fileMessage, this._useUpsert);
|
||||
});
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
const regularTypes = ['me_message', 'thread_broadcast'];
|
||||
|
||||
const isBotMessage = message.subtype && ['bot_message', 'slackbot_response'].includes(message.subtype);
|
||||
|
||||
if (message.subtype && !regularTypes.includes(message.subtype) && !isBotMessage) {
|
||||
if (await this.processMessageSubType(message, slackChannelId, newMessage, missedTypes)) {
|
||||
await this.converter.addMessage(newMessage, this._useUpsert);
|
||||
}
|
||||
} else {
|
||||
const text = this.convertSlackMessageToRocketChat(message.text);
|
||||
|
||||
if (isBotMessage) {
|
||||
newMessage.bot = true;
|
||||
}
|
||||
|
||||
if (message.subtype === 'me_message') {
|
||||
newMessage.msg = `_${text}_`;
|
||||
} else {
|
||||
newMessage.msg = text;
|
||||
}
|
||||
|
||||
if (message.thread_ts) {
|
||||
if (message.thread_ts === message.ts) {
|
||||
if (message.reply_users) {
|
||||
const replies = new Set<string>();
|
||||
message.reply_users.forEach((item: string) => {
|
||||
replies.add(this._replaceSlackUserId(item));
|
||||
});
|
||||
|
||||
if (replies.size) {
|
||||
newMessage.replies = Array.from(replies);
|
||||
}
|
||||
} else if (message.replies) {
|
||||
const replies = new Set<string>();
|
||||
message.replies.forEach((item: { user: string }) => {
|
||||
replies.add(this._replaceSlackUserId(item.user));
|
||||
});
|
||||
|
||||
if (replies.size) {
|
||||
newMessage.replies = Array.from(replies);
|
||||
}
|
||||
} else {
|
||||
this.logger.warn({
|
||||
msg: 'Failed to import the parent comment; missing replies/reply_users field',
|
||||
messageId: newMessage._id,
|
||||
});
|
||||
}
|
||||
|
||||
newMessage.tcount = message.reply_count;
|
||||
newMessage.tlm = new Date(parseInt(message.latest_reply.split('.')[0]) * 1000);
|
||||
} else {
|
||||
newMessage.tmid = this.makeSlackMessageId(slackChannelId, message.thread_ts);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.edited) {
|
||||
newMessage.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000);
|
||||
if (message.edited.user) {
|
||||
newMessage.editedBy = this._replaceSlackUserId(message.edited.user);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.attachments) {
|
||||
newMessage.attachments = this.convertMessageAttachments(message.attachments);
|
||||
}
|
||||
|
||||
if (message.icons?.emoji) {
|
||||
newMessage.emoji = message.icons.emoji;
|
||||
}
|
||||
|
||||
this.parseMentions(newMessage);
|
||||
await this.converter.addMessage(newMessage, this._useUpsert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_replaceSlackUserId(userId: string): string {
|
||||
if (userId === 'USLACKBOT') {
|
||||
return 'rocket.cat';
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
_replaceSlackUserIds(members: string[]) {
|
||||
if (!members?.length) {
|
||||
return [];
|
||||
}
|
||||
return members.map((userId) => this._replaceSlackUserId(userId));
|
||||
}
|
||||
|
||||
convertSlackMessageToRocketChat(message: string): string {
|
||||
if (message) {
|
||||
message = message.replace(/<!everyone>/g, '@all');
|
||||
message = message.replace(/<!channel>/g, '@all');
|
||||
message = message.replace(/<!here>/g, '@here');
|
||||
message = message.replace(/>/g, '>');
|
||||
message = message.replace(/</g, '<');
|
||||
message = message.replace(/&/g, '&');
|
||||
message = message.replace(/:simple_smile:/g, ':smile:');
|
||||
message = message.replace(/:memo:/g, ':pencil:');
|
||||
message = message.replace(/:piggy:/g, ':pig:');
|
||||
message = message.replace(/:uk:/g, ':gb:');
|
||||
message = message.replace(/<(http[s]?:[^>|]*)>/g, '$1');
|
||||
message = message.replace(/<(http[s]?:[^|]*)\|([^>]*)>/g, '[$2]($1)');
|
||||
message = message.replace(/<#([^|]*)\|([^>]*)>/g, '#$2');
|
||||
message = message.replace(/<@([^|]*)\|([^>]*)>/g, '@$1');
|
||||
message = message.replace(/<@([^|>]*)>/g, '@$1');
|
||||
} else {
|
||||
message = '';
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
convertSlackFileToPendingFile(file: SlackFile): IImportPendingFile {
|
||||
return {
|
||||
downloadUrl: file.url_private_download,
|
||||
id: file.id,
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
external: file.is_external,
|
||||
source: 'slack',
|
||||
original: {
|
||||
...file,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
convertMessageAttachments(attachments: SlackAttachment[]): IImportMessage['attachments'] {
|
||||
if (!attachments?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return attachments.map((attachment) => ({
|
||||
...attachment,
|
||||
text: this.convertSlackMessageToRocketChat(attachment.text),
|
||||
title: this.convertSlackMessageToRocketChat(attachment.title),
|
||||
fallback: this.convertSlackMessageToRocketChat(attachment.fallback),
|
||||
}));
|
||||
}
|
||||
}
|
||||
8
apps/meteor/server/lib/import/slack/index.ts
Normal file
8
apps/meteor/server/lib/import/slack/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Importers } from '..';
|
||||
import { SlackImporter } from './SlackImporter';
|
||||
|
||||
Importers.add({
|
||||
key: 'slack',
|
||||
name: 'Slack',
|
||||
importer: SlackImporter,
|
||||
});
|
||||
Reference in New Issue
Block a user