[NEW] Standard Importer Structure (#18357)

pull/21689/head
pierre-lehnen-rc 5 years ago committed by GitHub
parent cb61ac2b21
commit 14f7c3d509
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      app/importer-csv/server/adder.js
  2. 504
      app/importer-csv/server/importer.js
  3. 6
      app/importer-csv/server/index.js
  4. 5
      app/importer-hipchat-enterprise/server/adder.js
  5. 1219
      app/importer-hipchat-enterprise/server/importer.js
  6. 6
      app/importer-hipchat-enterprise/server/index.js
  7. 4
      app/importer-hipchat/client/adder.js
  8. 1
      app/importer-hipchat/client/index.js
  9. 7
      app/importer-hipchat/lib/info.js
  10. 5
      app/importer-hipchat/server/adder.js
  11. 375
      app/importer-hipchat/server/importer.js
  12. 1
      app/importer-hipchat/server/index.js
  13. 6
      app/importer-pending-avatars/server/importer.js
  14. 5
      app/importer-slack-users/server/adder.js
  15. 6
      app/importer-slack-users/server/index.js
  16. 5
      app/importer-slack/server/adder.js
  17. 1318
      app/importer-slack/server/importer.js
  18. 6
      app/importer-slack/server/index.js
  19. 832
      app/importer/server/classes/ImportDataConverter.ts
  20. 220
      app/importer/server/classes/ImporterBase.js
  21. 14
      app/importer/server/definitions/IImportChannel.ts
  22. 53
      app/importer/server/definitions/IImportMessage.ts
  23. 28
      app/importer/server/definitions/IImportRecord.ts
  24. 17
      app/importer/server/definitions/IImportUser.ts
  25. 2
      app/importer/server/index.js
  26. 26
      app/importer/server/methods/getImportFileData.js
  27. 88
      app/importer/server/models/ImportData.ts
  28. 6
      app/importer/server/startup/setImportsToInvalid.js
  29. 6
      app/lib/server/functions/insertMessage.js
  30. 2
      app/models/server/models/Rooms.js
  31. 1
      client/importPackages.ts
  32. 11
      client/views/admin/import/ImportHistoryPage.js
  33. 1
      server/importPackages.js

@ -1,5 +0,0 @@
import { CsvImporter } from './importer';
import { Importers } from '../../importer/server';
import { CsvImporterInfo } from '../lib/info';
Importers.add(new CsvImporterInfo(), CsvImporter);

@ -1,19 +1,11 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { Accounts } from 'meteor/accounts-base';
import {
RawImports,
Base,
ProgressStep,
Selection,
SelectionChannel,
SelectionUser,
ImporterWebsocket,
} from '../../importer/server';
import { Users, Rooms } from '../../models';
import { insertMessage } from '../../lib';
import { t } from '../../utils';
import { Users } from '../../models/server';
export class CsvImporter extends Base {
constructor(info, importRecord) {
@ -24,20 +16,17 @@ export class CsvImporter extends Base {
prepareUsingLocalFile(fullFilePath) {
this.logger.debug('start preparing import operation');
this.collection.remove({});
this.converter.clearImportData();
const zip = new this.AdmZip(fullFilePath);
const totalEntries = zip.getEntryCount();
ImporterWebsocket.progressUpdated({ rate: 0 });
let tempChannels = [];
let tempUsers = [];
let hasDirectMessages = false;
let count = 0;
let oldRate = 0;
const increaseCount = () => {
const increaseProgressCount = () => {
try {
count++;
const rate = Math.floor(count * 1000 / totalEntries) / 10;
@ -51,44 +40,93 @@ export class CsvImporter extends Base {
};
let messagesCount = 0;
let usersCount = 0;
let channelsCount = 0;
const dmRooms = new Map();
const roomIds = new Map();
const usedUsernames = new Set();
const availableUsernames = new Set();
const getRoomId = (roomName) => {
if (!roomIds.has(roomName)) {
roomIds.set(roomName, Random.id());
}
return roomIds.get(roomName);
};
zip.forEach((entry) => {
this.logger.debug(`Entry: ${ entry.entryName }`);
// Ignore anything that has `__MACOSX` in it's name, as sadly these things seem to mess everything up
if (entry.entryName.indexOf('__MACOSX') > -1) {
this.logger.debug(`Ignoring the file: ${ entry.entryName }`);
return increaseCount();
return increaseProgressCount();
}
// Directories are ignored, since they are "virtual" in a zip file
if (entry.isDirectory) {
this.logger.debug(`Ignoring the directory entry: ${ entry.entryName }`);
return increaseCount();
return increaseProgressCount();
}
// Parse the channels
if (entry.entryName.toLowerCase() === 'channels.csv') {
super.updateProgress(ProgressStep.PREPARING_CHANNELS);
const parsedChannels = this.csvParser(entry.getData().toString());
tempChannels = parsedChannels.map((c) => ({
id: Random.id(),
name: c[0].trim(),
creator: c[1].trim(),
isPrivate: c[2].trim().toLowerCase() === 'private',
members: c[3].trim().split(';').map((m) => m.trim()),
}));
return increaseCount();
channelsCount = parsedChannels.length;
for (const c of parsedChannels) {
const name = c[0].trim();
const id = getRoomId(name);
const creator = c[1].trim();
const isPrivate = c[2].trim().toLowerCase() === 'private';
const members = c[3].trim().split(';').map((m) => m.trim()).filter((m) => m);
this.converter.addChannel({
importIds: [
id,
],
u: {
_id: creator,
},
name,
users: members,
t: isPrivate ? 'p' : 'c',
});
}
super.updateRecord({ 'count.channels': channelsCount });
return increaseProgressCount();
}
// Parse the users
if (entry.entryName.toLowerCase() === 'users.csv') {
super.updateProgress(ProgressStep.PREPARING_USERS);
const parsedUsers = this.csvParser(entry.getData().toString());
tempUsers = parsedUsers.map((u) => ({ id: Random.id(), username: u[0].trim(), email: u[1].trim(), name: u[2].trim() }));
super.updateRecord({ 'count.users': tempUsers.length });
usersCount = parsedUsers.length;
for (const u of parsedUsers) {
const username = u[0].trim();
availableUsernames.add(username);
const email = u[1].trim();
const name = u[2].trim();
this.converter.addUser({
importIds: [
username,
],
emails: [
email,
],
username,
name,
});
}
return increaseCount();
super.updateRecord({ 'count.users': parsedUsers.length });
return increaseProgressCount();
}
// Parse the messages
@ -106,14 +144,15 @@ export class CsvImporter extends Base {
msgs = this.csvParser(entry.getData().toString());
} catch (e) {
this.logger.warn(`The file ${ entry.entryName } contains invalid syntax`, e);
return increaseCount();
return increaseProgressCount();
}
let data;
const msgGroupData = item[1].split('.')[0]; // messages
let isDirect = false;
if (folderName.toLowerCase() === 'directmessages') {
hasDirectMessages = true;
isDirect = true;
data = msgs.map((m) => ({ username: m[0], ts: m[2], text: m[3], otherUsername: m[1], isDirect: true }));
} else {
data = msgs.map((m) => ({ username: m[0], ts: m[1], text: m[2] }));
@ -124,370 +163,83 @@ export class CsvImporter extends Base {
super.updateRecord({ messagesstatus: channelName });
if (Base.getBSONSize(data) > Base.getMaxBSONSize()) {
Base.getBSONSafeArraysFromAnArray(data).forEach((splitMsg, i) => {
this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: `${ channelName }.${ i }`, messages: splitMsg, channel: folderName, i, msgGroupData });
});
} else {
this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'messages', name: channelName, messages: data, channel: folderName, msgGroupData });
}
super.updateRecord({ 'count.messages': messagesCount, messagesstatus: null });
return increaseCount();
}
increaseCount();
});
this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'users', users: tempUsers });
super.addCountToTotal(messagesCount + tempUsers.length);
ImporterWebsocket.progressUpdated({ rate: 100 });
if (hasDirectMessages) {
tempChannels.push({
id: '#directmessages#',
name: t('Direct_Messages'),
creator: 'rocket.cat',
isPrivate: false,
isDirect: true,
members: [],
});
}
// Insert the channels records.
this.collection.insert({ import: this.importRecord._id, importer: this.name, type: 'channels', channels: tempChannels });
super.updateRecord({ 'count.channels': tempChannels.length });
super.addCountToTotal(tempChannels.length);
// Ensure we have at least a single user, channel, or message
if (tempUsers.length === 0 && tempChannels.length === 0 && messagesCount === 0) {
this.logger.error('No users, channels, or messages found in the import file.');
super.updateProgress(ProgressStep.ERROR);
return super.getProgress();
}
const selectionUsers = tempUsers.map((u) => new SelectionUser(u.id, u.username, u.email, false, false, true));
const selectionChannels = tempChannels.map((c) => new SelectionChannel(c.id, c.name, false, true, c.isPrivate, undefined, c.isDirect));
const selectionMessages = this.importRecord.count.messages;
super.updateProgress(ProgressStep.USER_SELECTION);
return new Selection(this.name, selectionUsers, selectionChannels, selectionMessages);
}
startImport(importSelection) {
this.users = RawImports.findOne({ import: this.importRecord._id, type: 'users' });
this.channels = RawImports.findOne({ import: this.importRecord._id, type: 'channels' });
this.reloadCount();
const rawCollection = this.collection.model.rawCollection();
const distinct = Meteor.wrapAsync(rawCollection.distinct, rawCollection);
super.startImport(importSelection);
const started = Date.now();
// Ensure we're only going to import the users that the user has selected
for (const user of importSelection.users) {
for (const u of this.users.users) {
if (u.id === user.user_id) {
u.do_import = user.do_import;
}
}
}
this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } });
// Ensure we're only importing the channels the user has selected.
for (const channel of importSelection.channels) {
for (const c of this.channels.channels) {
if (c.id === channel.channel_id) {
c.do_import = channel.do_import;
}
}
}
this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } });
const startedByUserId = Meteor.userId();
Meteor.defer(() => {
super.updateProgress(ProgressStep.IMPORTING_USERS);
try {
// Import the users
for (const u of this.users.users) {
if (!u.do_import) {
continue;
}
Meteor.runAsUser(startedByUserId, () => {
let existantUser = Users.findOneByEmailAddress(u.email);
// If we couldn't find one by their email address, try to find an existing user by their username
if (!existantUser) {
existantUser = Users.findOneByUsernameIgnoringCase(u.username);
}
if (existantUser) {
// since we have an existing user, let's try a few things
u.rocketId = existantUser._id;
Users.update({ _id: u.rocketId }, { $addToSet: { importIds: u.id } });
} else {
const userId = Accounts.createUser({ email: u.email, password: Date.now() + u.name + u.email.toUpperCase() });
Meteor.runAsUser(userId, () => {
Meteor.call('setUsername', u.username, { joinDefaultChannelsSilenced: true });
Users.setName(userId, u.name);
Users.update({ _id: userId }, { $addToSet: { importIds: u.id } });
u.rocketId = userId;
});
}
super.addCountCompleted(1);
});
}
this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } });
// Import the channels
super.updateProgress(ProgressStep.IMPORTING_CHANNELS);
for (const c of this.channels.channels) {
if (!c.do_import) {
continue;
}
if (c.isDirect) {
super.addCountCompleted(1);
continue;
}
Meteor.runAsUser(startedByUserId, () => {
const existantRoom = Rooms.findOneByName(c.name);
// If the room exists or the name of it is 'general', then we don't need to create it again
if (existantRoom || c.name.toUpperCase() === 'GENERAL') {
c.rocketId = c.name.toUpperCase() === 'GENERAL' ? 'GENERAL' : existantRoom._id;
Rooms.update({ _id: c.rocketId }, { $addToSet: { importIds: c.id } });
} else {
// Find the rocketchatId of the user who created this channel
let creatorId = startedByUserId;
for (const u of this.users.users) {
if (u.username === c.creator && u.do_import) {
creatorId = u.rocketId;
}
}
// Create the channel
Meteor.runAsUser(creatorId, () => {
const roomInfo = Meteor.call(c.isPrivate ? 'createPrivateGroup' : 'createChannel', c.name, c.members);
c.rocketId = roomInfo.rid;
if (isDirect) {
for (const msg of data) {
const sourceId = [msg.username, msg.otherUsername].sort().join('/');
if (!dmRooms.has(sourceId)) {
this.converter.addChannel({
importIds: [
sourceId,
],
users: [msg.username, msg.otherUsername],
t: 'd',
});
Rooms.update({ _id: c.rocketId }, { $addToSet: { importIds: c.id } });
dmRooms.set(sourceId, true);
}
super.addCountCompleted(1);
});
}
this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } });
// If no channels file, collect channel map from DB for message-only import
if (this.channels.channels.length === 0) {
const channelNames = distinct('channel', { import: this.importRecord._id, type: 'messages', channel: { $ne: 'directMessages' } });
for (const cname of channelNames) {
Meteor.runAsUser(startedByUserId, () => {
const existantRoom = Rooms.findOneByName(cname);
if (existantRoom || cname.toUpperCase() === 'GENERAL') {
this.channels.channels.push({
id: cname.replace('.', '_'),
name: cname,
rocketId: cname.toUpperCase() === 'GENERAL' ? 'GENERAL' : existantRoom._id,
do_import: true,
});
}
});
const newMessage = {
rid: sourceId,
u: {
_id: msg.username,
},
ts: new Date(parseInt(msg.ts)),
msg: msg.text,
};
usedUsernames.add(msg.username);
usedUsernames.add(msg.otherUsername);
this.converter.addMessage(newMessage);
}
}
// If no users file, collect user map from DB for message-only import
if (this.users.users.length === 0) {
const usernames = distinct('messages.username', { import: this.importRecord._id, type: 'messages' });
for (const username of usernames) {
Meteor.runAsUser(startedByUserId, () => {
if (!this.getUserFromUsername(username)) {
const user = Users.findOneByUsernameIgnoringCase(username);
if (user) {
this.users.users.push({
rocketId: user._id,
username: user.username,
});
}
}
});
} else {
const rid = getRoomId(folderName);
for (const msg of data) {
const newMessage = {
rid,
u: {
_id: msg.username,
},
ts: new Date(parseInt(msg.ts)),
msg: msg.text,
};
usedUsernames.add(msg.username);
this.converter.addMessage(newMessage);
}
}
// Import the Messages
super.updateProgress(ProgressStep.IMPORTING_MESSAGES);
const messagePacks = this.collection.find({ import: this.importRecord._id, type: 'messages' });
messagePacks.forEach((pack) => {
const ch = pack.channel;
const { msgGroupData } = pack;
const csvChannel = this.getChannelFromName(ch);
if (!csvChannel || !csvChannel.do_import) {
return;
}
if (csvChannel.isDirect) {
this._importDirectMessagesFile(msgGroupData, pack, startedByUserId);
return;
}
if (ch.toLowerCase() === 'directmessages') {
return;
}
const room = Rooms.findOneById(csvChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } });
const timestamps = {};
Meteor.runAsUser(startedByUserId, () => {
super.updateRecord({ messagesstatus: `${ ch }/${ msgGroupData }.${ pack.messages.length }` });
for (const msg of pack.messages) {
if (isNaN(new Date(parseInt(msg.ts)))) {
this.logger.warn(`Timestamp on a message in ${ ch }/${ msgGroupData } is invalid`);
super.addCountCompleted(1);
continue;
}
const creator = this.getUserFromUsername(msg.username);
if (creator) {
let suffix = '';
if (timestamps[msg.ts] === undefined) {
timestamps[msg.ts] = 1;
} else {
suffix = `-${ timestamps[msg.ts] }`;
timestamps[msg.ts] += 1;
}
const msgObj = {
_id: `csv-${ csvChannel.id }-${ msg.ts }${ suffix }`,
ts: new Date(parseInt(msg.ts)),
msg: msg.text,
rid: room._id,
u: {
_id: creator._id,
username: creator.username,
},
};
insertMessage(creator, msgObj, room, true);
}
super.addCountCompleted(1);
}
});
});
super.updateProgress(ProgressStep.FINISHING);
super.updateProgress(ProgressStep.DONE);
} catch (e) {
this.logger.error(e);
super.updateProgress(ProgressStep.ERROR);
super.updateRecord({ 'count.messages': messagesCount, messagesstatus: null });
return increaseProgressCount();
}
const timeTook = Date.now() - started;
this.logger.log(`CSV Import took ${ timeTook } milliseconds.`);
increaseProgressCount();
});
return super.getProgress();
}
_importDirectMessagesFile(msgGroupData, msgs, startedByUserId) {
const dmUsers = {};
const findUser = (username) => {
if (!dmUsers[username]) {
const user = this.getUserFromUsername(username) || Users.findOneByUsername(username, { fields: { username: 1 } });
dmUsers[username] = user;
}
return dmUsers[username];
};
Meteor.runAsUser(startedByUserId, () => {
const timestamps = {};
let room;
let rid;
super.updateRecord({ messagesstatus: `${ t('Direct_Messagest') }/${ msgGroupData }.${ msgs.messages.length }` });
for (const msg of msgs.messages) {
if (isNaN(new Date(parseInt(msg.ts)))) {
this.logger.warn(`Timestamp on a message in ${ t('Direct_Messagest') }/${ msgGroupData } is invalid`);
super.addCountCompleted(1);
continue;
}
const creator = findUser(msg.username);
const targetUser = findUser(msg.otherUsername);
if (creator && targetUser) {
if (!rid) {
const roomInfo = Meteor.runAsUser(creator._id, () => Meteor.call('createDirectMessage', targetUser.username));
rid = roomInfo.rid;
room = Rooms.findOneById(rid, { fields: { usernames: 1, t: 1, name: 1 } });
}
if (!room) {
this.logger.warn(`DM room not found for users ${ msg.username } and ${ msg.otherUsername }`);
super.addCountCompleted(1);
continue;
}
let suffix = '';
if (timestamps[msg.ts] === undefined) {
timestamps[msg.ts] = 1;
} else {
suffix = `-${ timestamps[msg.ts] }`;
timestamps[msg.ts] += 1;
}
const msgObj = {
_id: `csv-${ rid }-${ msg.ts }${ suffix }`,
ts: new Date(parseInt(msg.ts)),
msg: msg.text,
rid: room._id,
u: {
_id: creator._id,
username: creator.username,
},
};
insertMessage(creator, msgObj, room, true);
}
super.addCountCompleted(1);
// Check if any of the message usernames was not in the imported list of users
for (const username of usedUsernames) {
if (availableUsernames.has(username)) {
continue;
}
});
}
getChannelFromName(channelName) {
if (channelName.toLowerCase() === 'directmessages') {
return this.getDirectMessagesChannel();
}
for (const ch of this.channels.channels) {
if (ch.name === channelName) {
return ch;
// Check if an user with that username already exists
const user = Users.findOneByUsername(username);
if (user && !user.importIds?.includes(username)) {
// Add the username to the local user's importIds so it can be found by the import process
// This way we can support importing new messages for existing users
Users.addImportIds(user._id, username);
}
}
}
getDirectMessagesChannel() {
for (const ch of this.channels.channels) {
if (ch.is_direct || ch.isDirect) {
return ch;
}
}
}
super.addCountToTotal(messagesCount + usersCount + channelsCount);
ImporterWebsocket.progressUpdated({ rate: 100 });
getUserFromUsername(username) {
for (const u of this.users.users) {
if (u.username === username) {
return Users.findOneById(u.rocketId, { fields: { username: 1 } });
}
// Ensure we have at least a single user, channel, or message
if (usersCount === 0 && channelsCount === 0 && messagesCount === 0) {
this.logger.error('No users, channels, or messages found in the import file.');
super.updateProgress(ProgressStep.ERROR);
return super.getProgress();
}
}
}

@ -1 +1,5 @@
import './adder';
import { CsvImporter } from './importer';
import { Importers } from '../../importer/server';
import { CsvImporterInfo } from '../lib/info';
Importers.add(new CsvImporterInfo(), CsvImporter);

@ -1,5 +0,0 @@
import { HipChatEnterpriseImporter } from './importer';
import { Importers } from '../../importer/server';
import { HipChatEnterpriseImporterInfo } from '../lib/info';
Importers.add(new HipChatEnterpriseImporterInfo(), HipChatEnterpriseImporter);

File diff suppressed because it is too large Load Diff

@ -1 +1,5 @@
import './adder';
import { HipChatEnterpriseImporter } from './importer';
import { Importers } from '../../importer/server';
import { HipChatEnterpriseImporterInfo } from '../lib/info';
Importers.add(new HipChatEnterpriseImporterInfo(), HipChatEnterpriseImporter);

@ -1,4 +0,0 @@
import { Importers } from '../../importer/client';
import { HipChatImporterInfo } from '../lib/info';
Importers.add(new HipChatImporterInfo());

@ -1,7 +0,0 @@
import { ImporterInfo } from '../../importer/lib/ImporterInfo';
export class HipChatImporterInfo extends ImporterInfo {
constructor() {
super('hipchat', 'HipChat (zip)', 'application/zip');
}
}

@ -1,5 +0,0 @@
import { HipChatImporter } from './importer';
import { Importers } from '../../importer/server';
import { HipChatImporterInfo } from '../lib/info';
Importers.add(new HipChatImporterInfo(), HipChatImporter);

@ -1,375 +0,0 @@
import limax from 'limax';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import _ from 'underscore';
import moment from 'moment';
import {
RawImports,
Base,
ProgressStep,
Selection,
SelectionChannel,
SelectionUser,
} from '../../importer/server';
import { RocketChatFile } from '../../file';
import { Users, Rooms } from '../../models';
import { sendMessage } from '../../lib';
import 'moment-timezone';
export class HipChatImporter extends Base {
constructor(info, importRecord) {
super(info, importRecord);
this.userTags = [];
this.roomPrefix = 'hipchat_export/rooms/';
this.usersPrefix = 'hipchat_export/users/';
}
prepare(dataURI, sentContentType, fileName, skipTypeCheck) {
super.prepare(dataURI, sentContentType, fileName, skipTypeCheck);
const { image } = RocketChatFile.dataURIParse(dataURI);
// const contentType = ref.contentType;
const zip = new this.AdmZip(Buffer.from(image, 'base64'));
const zipEntries = zip.getEntries();
let tempRooms = [];
let tempUsers = [];
const tempMessages = {};
zipEntries.forEach((entry) => {
if (entry.entryName.indexOf('__MACOSX') > -1) {
this.logger.debug(`Ignoring the file: ${ entry.entryName }`);
}
if (entry.isDirectory) {
return;
}
if (entry.entryName.indexOf(this.roomPrefix) > -1) {
let roomName = entry.entryName.split(this.roomPrefix)[1];
if (roomName === 'list.json') {
super.updateProgress(ProgressStep.PREPARING_CHANNELS);
tempRooms = JSON.parse(entry.getData().toString()).rooms;
tempRooms.forEach((room) => {
room.name = limax(room.name);
});
} else if (roomName.indexOf('/') > -1) {
const item = roomName.split('/');
roomName = limax(item[0]);
const msgGroupData = item[1].split('.')[0];
if (!tempMessages[roomName]) {
tempMessages[roomName] = {};
}
try {
tempMessages[roomName][msgGroupData] = JSON.parse(entry.getData().toString());
return tempMessages[roomName][msgGroupData];
} catch (error) {
return this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`);
}
}
} else if (entry.entryName.indexOf(this.usersPrefix) > -1) {
const usersName = entry.entryName.split(this.usersPrefix)[1];
if (usersName === 'list.json') {
super.updateProgress(ProgressStep.PREPARING_USERS);
tempUsers = JSON.parse(entry.getData().toString()).users;
return tempUsers;
}
return this.logger.warn(`Unexpected file in the ${ this.name } import: ${ entry.entryName }`);
}
});
const usersId = this.collection.insert({
import: this.importRecord._id,
importer: this.name,
type: 'users',
users: tempUsers,
});
this.users = this.collection.findOne(usersId);
this.updateRecord({
'count.users': tempUsers.length,
});
this.addCountToTotal(tempUsers.length);
const channelsId = this.collection.insert({
import: this.importRecord._id,
importer: this.name,
type: 'channels',
channels: tempRooms,
});
this.channels = this.collection.findOne(channelsId);
this.updateRecord({
'count.channels': tempRooms.length,
});
this.addCountToTotal(tempRooms.length);
super.updateProgress(ProgressStep.PREPARING_MESSAGES);
let messagesCount = 0;
Object.keys(tempMessages).forEach((channel) => {
const messagesObj = tempMessages[channel];
Object.keys(messagesObj).forEach((date) => {
const msgs = messagesObj[date];
messagesCount += msgs.length;
this.updateRecord({
messagesstatus: `${ channel }/${ date }`,
});
if (Base.getBSONSize(msgs) > Base.getMaxBSONSize()) {
Base.getBSONSafeArraysFromAnArray(msgs).forEach((splitMsg, i) => {
this.collection.insert({
import: this.importRecord._id,
importer: this.name,
type: 'messages',
name: `${ channel }/${ date }.${ i }`,
messages: splitMsg,
channel,
date,
i,
});
});
} else {
this.collection.insert({
import: this.importRecord._id,
importer: this.name,
type: 'messages',
name: `${ channel }/${ date }`,
messages: msgs,
channel,
date,
});
}
});
});
this.updateRecord({
'count.messages': messagesCount,
messagesstatus: null,
});
this.addCountToTotal(messagesCount);
if (tempUsers.length === 0 || tempRooms.length === 0 || messagesCount === 0) {
this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempRooms.length }, and the loaded messages ${ messagesCount }`);
super.updateProgress(ProgressStep.ERROR);
return this.getProgress();
}
const selectionUsers = tempUsers.map(function(user) {
return new SelectionUser(user.user_id, user.name, user.email, user.is_deleted, false, !user.is_bot);
});
const selectionChannels = tempRooms.map(function(room) {
return new SelectionChannel(room.room_id, room.name, room.is_archived, true, false);
});
const selectionMessages = this.importRecord.count.messages;
super.updateProgress(ProgressStep.USER_SELECTION);
return new Selection(this.name, selectionUsers, selectionChannels, selectionMessages);
}
startImport(importSelection) {
this.users = RawImports.findOne({ import: this.importRecord._id, type: 'users' });
this.channels = RawImports.findOne({ import: this.importRecord._id, type: 'channels' });
this.reloadCount();
super.startImport(importSelection);
const start = Date.now();
importSelection.users.forEach((user) => {
this.users.users.forEach((u) => {
if (u.user_id === user.user_id) {
u.do_import = user.do_import;
}
});
});
this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } });
importSelection.channels.forEach((channel) =>
this.channels.channels.forEach((c) => {
if (c.room_id === channel.channel_id) {
c.do_import = channel.do_import;
}
}),
);
this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } });
const startedByUserId = Meteor.userId();
Meteor.defer(() => {
super.updateProgress(ProgressStep.IMPORTING_USERS);
try {
this.users.users.forEach((user) => {
if (!user.do_import) {
return;
}
Meteor.runAsUser(startedByUserId, () => {
const existantUser = Users.findOneByEmailAddress(user.email);
if (existantUser) {
user.rocketId = existantUser._id;
this.userTags.push({
hipchat: `@${ user.mention_name }`,
rocket: `@${ existantUser.username }`,
});
} else {
const userId = Accounts.createUser({
email: user.email,
password: Date.now() + user.name + user.email.toUpperCase(),
});
user.rocketId = userId;
this.userTags.push({
hipchat: `@${ user.mention_name }`,
rocket: `@${ user.mention_name }`,
});
Meteor.runAsUser(userId, () => {
Meteor.call('setUsername', user.mention_name, {
joinDefaultChannelsSilenced: true,
});
Meteor.call('setAvatarFromService', user.photo_url, undefined, 'url');
return Meteor.call('userSetUtcOffset', parseInt(moment().tz(user.timezone).format('Z').toString().split(':')[0]));
});
if (user.name != null) {
Users.setName(userId, user.name);
}
if (user.is_deleted) {
Meteor.call('setUserActiveStatus', userId, false);
}
}
return this.addCountCompleted(1);
});
});
this.collection.update({ _id: this.users._id }, { $set: { users: this.users.users } });
const channelNames = [];
super.updateProgress(ProgressStep.IMPORTING_CHANNELS);
this.channels.channels.forEach((channel) => {
if (!channel.do_import) {
return;
}
channelNames.push(channel.name);
Meteor.runAsUser(startedByUserId, () => {
channel.name = channel.name.replace(/ /g, '');
const existantRoom = Rooms.findOneByName(channel.name);
if (existantRoom) {
channel.rocketId = existantRoom._id;
} else {
let userId = '';
this.users.users.forEach((user) => {
if (user.user_id === channel.owner_user_id) {
userId = user.rocketId;
}
});
if (userId === '') {
this.logger.warn(`Failed to find the channel creator for ${ channel.name }, setting it to the current running user.`);
userId = startedByUserId;
}
Meteor.runAsUser(userId, () => {
const returned = Meteor.call('createChannel', channel.name, []);
channel.rocketId = returned.rid;
});
Rooms.update({
_id: channel.rocketId,
}, {
$set: {
ts: new Date(channel.created * 1000),
},
});
}
return this.addCountCompleted(1);
});
});
this.collection.update({ _id: this.channels._id }, { $set: { channels: this.channels.channels } });
super.updateProgress(ProgressStep.IMPORTING_MESSAGES);
const nousers = {};
for (const channel of channelNames) {
const hipchatChannel = this.getHipChatChannelFromName(channel);
if (!hipchatChannel || !hipchatChannel.do_import) {
continue;
}
const room = Rooms.findOneById(hipchatChannel.rocketId, {
fields: {
usernames: 1,
t: 1,
name: 1,
},
});
const messagePacks = this.collection.find({ import: this.importRecord._id, type: 'messages', channel });
Meteor.runAsUser(startedByUserId, () => {
messagePacks.forEach((pack) => {
const packId = pack.i ? `${ pack.date }.${ pack.i }` : pack.date;
this.updateRecord({ messagesstatus: `${ channel }/${ packId } (${ pack.messages.length })` });
pack.messages.forEach((message) => {
if (message.from != null) {
const user = this.getRocketUser(message.from.user_id);
if (user != null) {
const msgObj = {
msg: this.convertHipChatMessageToRocketChat(message.message),
ts: new Date(message.date),
u: {
_id: user._id,
username: user.username,
},
};
sendMessage(user, msgObj, room, true);
} else if (!nousers[message.from.user_id]) {
nousers[message.from.user_id] = message.from;
}
} else if (!_.isArray(message)) {
console.warn('Please report the following:', message);
}
this.addCountCompleted(1);
});
});
});
}
this.logger.warn('The following did not have users:', nousers);
super.updateProgress(ProgressStep.FINISHING);
this.channels.channels.forEach((channel) => {
if (channel.do_import && channel.is_archived) {
Meteor.runAsUser(startedByUserId, () => Meteor.call('archiveRoom', channel.rocketId));
}
});
super.updateProgress(ProgressStep.DONE);
} catch (e) {
this.logger.error(e);
super.updateProgress(ProgressStep.ERROR);
}
const timeTook = Date.now() - start;
return this.logger.log(`Import took ${ timeTook } milliseconds.`);
});
return this.getProgress();
}
getHipChatChannelFromName(channelName) {
return this.channels.channels.find((channel) => channel.name === channelName);
}
getRocketUser(hipchatId) {
const user = this.users.users.find((user) => user.user_id === hipchatId);
return user ? Users.findOneById(user.rocketId, {
fields: {
username: 1,
name: 1,
},
}) : undefined;
}
convertHipChatMessageToRocketChat(message) {
if (message != null) {
this.userTags.forEach((userReplace) => {
message = message.replace(userReplace.hipchat, userReplace.rocket);
});
} else {
message = '';
}
return message;
}
}

@ -8,12 +8,6 @@ import {
import { Users } from '../../models';
export class PendingAvatarImporter extends Base {
constructor(info, importRecord) {
super(info, importRecord);
this.userTags = [];
this.bots = {};
}
prepareFileCount() {
this.logger.debug('start preparing import operation');
super.updateProgress(ProgressStep.PREPARING_STARTED);

@ -1,5 +0,0 @@
import { SlackUsersImporter } from './importer';
import { Importers } from '../../importer/server';
import { SlackUsersImporterInfo } from '../lib/info';
Importers.add(new SlackUsersImporterInfo(), SlackUsersImporter);

@ -1 +1,5 @@
import './adder';
import { SlackUsersImporter } from './importer';
import { Importers } from '../../importer/server';
import { SlackUsersImporterInfo } from '../lib/info';
Importers.add(new SlackUsersImporterInfo(), SlackUsersImporter);

@ -1,5 +0,0 @@
import { SlackImporter } from './importer';
import { Importers } from '../../importer/server';
import { SlackImporterInfo } from '../lib/info';
Importers.add(new SlackImporterInfo(), SlackImporter);

File diff suppressed because it is too large Load Diff

@ -1 +1,5 @@
import './adder';
import { SlackImporter } from './importer';
import { Importers } from '../../importer/server';
import { SlackImporterInfo } from '../lib/info';
Importers.add(new SlackImporterInfo(), SlackImporter);

@ -0,0 +1,832 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import _ from 'underscore';
import { ImportData } from '../models/ImportData';
import { IImportUser } from '../definitions/IImportUser';
import { IImportMessage, IImportMessageReaction } from '../definitions/IImportMessage';
import { IImportChannel } from '../definitions/IImportChannel';
import { IImportUserRecord, IImportChannelRecord, IImportMessageRecord } from '../definitions/IImportRecord';
import { Users, Rooms, Subscriptions } from '../../../models/server';
import { generateUsernameSuggestion, insertMessage } from '../../../lib/server';
import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus';
import { IUser } from '../../../../definition/IUser';
type IRoom = Record<string, any>;
type IMessage = Record<string, any>;
type IUserIdentification = {
_id: string;
username: string | undefined;
};
type IMentionedUser = {
_id: string;
username: string;
name?: string;
};
type IMentionedChannel = {
_id: string;
name: string;
};
type IMessageReaction = {
name: string;
usernames: Array<string>;
};
type IMessageReactions = Record<string, IMessageReaction>;
interface IConversionCallbacks {
beforeImportFn?: {
(data: IImportUser | IImportChannel | IImportMessage, type: string): boolean;
};
afterImportFn?: {
(data: IImportUser | IImportChannel | IImportMessage, type: string): void;
};
}
const guessNameFromUsername = (username: string): string =>
username
.replace(/\W/g, ' ')
.replace(/\s(.)/g, (u) => u.toUpperCase())
.replace(/^(.)/, (u) => u.toLowerCase())
.replace(/^\w/, (u) => u.toUpperCase());
export class ImportDataConverter {
private _userCache: Map<string, IUserIdentification>;
// display name uses a different cache because it's only used on mentions so we don't need to load it every time we load an user
private _userDisplayNameCache: Map<string, string>;
private _roomCache: Map<string, string>;
private _roomNameCache: Map<string, string>;
constructor() {
this._userCache = new Map();
this._userDisplayNameCache = new Map();
this._roomCache = new Map();
this._roomNameCache = new Map();
}
addUserToCache(importId: string, _id: string, username: string | undefined): IUserIdentification {
const cache = {
_id,
username,
};
this._userCache.set(importId, cache);
return cache;
}
addUserDisplayNameToCache(importId: string, name: string): string {
this._userDisplayNameCache.set(importId, name);
return name;
}
addRoomToCache(importId: string, rid: string): string {
this._roomCache.set(importId, rid);
return rid;
}
addRoomNameToCache(importId: string, name: string): string {
this._roomNameCache.set(importId, name);
return name;
}
addUserDataToCache(userData: IImportUser): void {
if (!userData._id) {
return;
}
if (!userData.importIds.length) {
return;
}
this.addUserToCache(userData.importIds[0], userData._id, userData.username);
}
addObject(type: string, data: Record<string, any>, options: Record<string, any> = {}): void {
ImportData.model.rawCollection().insert({
data,
dataType: type,
...options,
});
}
addUser(data: IImportUser): void {
this.addObject('user', data);
}
addChannel(data: IImportChannel): void {
this.addObject('channel', data);
}
addMessage(data: IImportMessage, useQuickInsert = false): void {
this.addObject('message', data, {
useQuickInsert: useQuickInsert || undefined,
});
}
updateUserId(_id: string, userData: IImportUser): void {
const updateData: Record<string, any> = {
$set: {
statusText: userData.statusText || undefined,
roles: userData.roles || ['user'],
type: userData.type || 'user',
bio: userData.bio || undefined,
name: userData.name || undefined,
},
};
if (userData.importIds?.length) {
updateData.$addToSet = {
importIds: {
$each: userData.importIds,
},
};
}
Users.update({ _id }, updateData);
}
updateUser(existingUser: IUser, userData: IImportUser): void {
userData._id = existingUser._id;
this.updateUserId(userData._id, userData);
if (userData.importIds.length) {
this.addUserToCache(userData.importIds[0], existingUser._id, existingUser.username);
}
if (userData.avatarUrl) {
try {
Users.update({ _id: existingUser._id }, { $set: { _pendingAvatarUrl: userData.avatarUrl } });
} catch (error) {
console.warn(`Failed to set ${ existingUser._id }'s avatar from url ${ userData.avatarUrl }`);
console.error(error);
}
}
}
insertUser(userData: IImportUser): IUser {
const password = `${ Date.now() }${ userData.name || '' }${ userData.emails.length ? userData.emails[0].toUpperCase() : '' }`;
const userId = userData.emails.length ? Accounts.createUser({
email: userData.emails[0],
password,
}) : Accounts.createUser({
username: userData.username,
password,
// @ts-ignore
joinDefaultChannelsSilenced: true,
});
userData._id = userId;
const user = Users.findOneById(userId, {});
if (user && userData.importIds.length) {
this.addUserToCache(userData.importIds[0], user._id, userData.username);
}
Meteor.runAsUser(userId, () => {
Meteor.call('setUsername', userData.username, { joinDefaultChannelsSilenced: true });
if (userData.name) {
Users.setName(userId, userData.name);
}
this.updateUserId(userId, userData);
if (userData.utcOffset) {
Users.setUtcOffset(userId, userData.utcOffset);
}
if (userData.avatarUrl) {
try {
Users.update({ _id: userId }, { $set: { _pendingAvatarUrl: userData.avatarUrl } });
} catch (error) {
console.warn(`Failed to set ${ userId }'s avatar from url ${ userData.avatarUrl }`);
console.error(error);
}
}
});
return user;
}
convertUsers({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const users = ImportData.find({ dataType: 'user' });
users.forEach(({ data, _id }: IImportUserRecord) => {
try {
if (beforeImportFn && !beforeImportFn(data, 'user')) {
this.skipRecord(_id);
return;
}
data.emails = data.emails.filter((item) => item);
data.importIds = data.importIds.filter((item) => item);
if (!data.emails.length && !data.username) {
throw new Error('importer-user-missing-email-and-username');
}
let existingUser;
if (data.emails.length) {
existingUser = Users.findOneByEmailAddress(data.emails[0], {});
}
if (data.username) {
// If we couldn't find one by their email address, try to find an existing user by their username
if (!existingUser) {
existingUser = Users.findOneByUsernameIgnoringCase(data.username, {});
}
} else {
data.username = generateUsernameSuggestion({
name: data.name,
emails: data.emails,
});
}
if (existingUser) {
this.updateUser(existingUser, data);
} else {
if (!data.name && data.username) {
data.name = guessNameFromUsername(data.username);
}
existingUser = this.insertUser(data);
}
// Deleted users are 'inactive' users in Rocket.Chat
if (data.deleted && existingUser?.active) {
setUserActiveStatus(data._id, false, true);
}
if (afterImportFn) {
afterImportFn(data, 'user');
}
} catch (e) {
this.saveError(_id, e);
}
});
}
saveNewId(importId: string, newId: string): void {
ImportData.update({
_id: importId,
}, {
$set: {
id: newId,
},
});
}
saveError(importId: string, error: Error): void {
console.error(error);
ImportData.update({
_id: importId,
}, {
$push: {
errors: {
message: error.message,
stack: error.stack,
},
},
});
}
skipRecord(_id: string): void {
ImportData.update({
_id,
}, {
$set: {
skipped: true,
},
});
}
convertMessageReactions(importedReactions: Record<string, IImportMessageReaction>): undefined | IMessageReactions {
const reactions: IMessageReactions = {};
for (const name in importedReactions) {
if (!importedReactions.hasOwnProperty(name)) {
continue;
}
const { users } = importedReactions[name];
if (!users.length) {
continue;
}
const reaction: IMessageReaction = {
name,
usernames: [],
};
for (const importId of users) {
const username = this.findImportedUsername(importId);
if (username && !reaction.usernames.includes(username)) {
reaction.usernames.push(username);
}
}
if (reaction.usernames.length) {
reactions[name] = reaction;
}
}
if (Object.keys(reactions).length > 0) {
return reactions;
}
}
convertMessageReplies(replies: Array<string>): Array<string> {
const result: Array<string> = [];
for (const importId of replies) {
const userId = this.findImportedUserId(importId);
if (userId && !result.includes(userId)) {
result.push(userId);
}
}
return result;
}
convertMessageMentions(message: IImportMessage): Array<IMentionedUser> | undefined {
const { mentions } = message;
if (!mentions) {
return undefined;
}
const result: Array<IMentionedUser> = [];
for (const importId of mentions) {
// eslint-disable-next-line no-extra-parens
if (importId === ('all' as 'string') || importId === 'here') {
result.push({
_id: importId,
username: importId,
});
continue;
}
// Loading the name will also store the remaining data on the cache if it's missing, so this won't run two queries
const name = this.findImportedUserDisplayName(importId);
const data = this.findImportedUser(importId);
if (!data) {
throw new Error('importer-message-mentioned-user-not-found');
}
if (!data.username) {
throw new Error('importer-message-mentioned-username-not-found');
}
message.msg = message.msg.replace(new RegExp(`\@${ importId }`, 'gi'), `@${ data.username }`);
result.push({
_id: data._id,
username: data.username as 'string',
name,
});
}
return result;
}
convertMessageChannels(message: IImportMessage): Array<IMentionedChannel> | undefined {
const { channels } = message;
if (!channels) {
return;
}
const result: Array<IMentionedChannel> = [];
for (const importId of channels) {
// loading the name will also store the id on the cache if it's missing, so this won't run two queries
const name = this.findImportedRoomName(importId);
const _id = this.findImportedRoomId(importId);
if (!_id || !name) {
console.warn(`Mentioned room not found: ${ importId }`);
continue;
}
message.msg = message.msg.replace(new RegExp(`\#${ importId }`, 'gi'), `#${ name }`);
result.push({
_id,
name,
});
}
return result;
}
convertMessages({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const rids: Array<string> = [];
const messages = ImportData.find({ dataType: 'message' });
messages.forEach(({ data: m, _id }: IImportMessageRecord) => {
try {
if (beforeImportFn && !beforeImportFn(m, 'message')) {
this.skipRecord(_id);
return;
}
if (!m.ts || isNaN(m.ts as unknown as number)) {
throw new Error('importer-message-invalid-timestamp');
}
const creator = this.findImportedUser(m.u._id);
if (!creator) {
console.warn(`Imported user not found: ${ m.u._id }`);
throw new Error('importer-message-unknown-user');
}
const rid = this.findImportedRoomId(m.rid);
if (!rid) {
throw new Error('importer-message-unknown-room');
}
if (!rids.includes(rid)) {
rids.push(rid);
}
// Convert the mentions and channels first because these conversions can also modify the msg in the message object
const mentions = m.mentions && this.convertMessageMentions(m);
const channels = m.channels && this.convertMessageChannels(m);
const msgObj: IMessage = {
rid,
u: {
_id: creator._id,
username: creator.username,
},
msg: m.msg,
ts: m.ts,
t: m.t || undefined,
groupable: m.groupable,
tmid: m.tmid,
tlm: m.tlm,
tcount: m.tcount,
replies: m.replies && this.convertMessageReplies(m.replies),
editedAt: m.editedAt,
editedBy: m.editedBy && (this.findImportedUser(m.editedBy) || undefined),
mentions,
channels,
_importFile: m._importFile,
url: m.url,
attachments: m.attachments,
bot: m.bot,
emoji: m.emoji,
alias: m.alias,
};
if (m._id) {
msgObj._id = m._id;
}
if (m.reactions) {
msgObj.reactions = this.convertMessageReactions(m.reactions);
}
try {
insertMessage(creator, msgObj, rid, true);
} catch (e) {
console.warn(`Failed to import message with timestamp ${ String(msgObj.ts) } to room ${ rid }`);
console.error(e);
}
if (afterImportFn) {
afterImportFn(m, 'message');
}
} catch (e) {
this.saveError(_id, e);
}
});
for (const rid of rids) {
try {
Rooms.resetLastMessageById(rid);
} catch (e) {
console.warn(`Failed to update last message of room ${ rid }`);
console.error(e);
}
}
}
updateRoom(room: IRoom, roomData: IImportChannel, startedByUserId: string): void {
roomData._id = room._id;
// eslint-disable-next-line no-extra-parens
if ((roomData._id as string).toUpperCase() === 'GENERAL' && roomData.name !== room.name) {
Meteor.runAsUser(startedByUserId, () => {
Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', roomData.name);
});
}
this.updateRoomId(room._id, roomData);
}
findDMForImportedUsers(...users: Array<string>): IImportChannel | undefined {
const record = ImportData.findDMForImportedUsers(...users);
if (record) {
return record.data;
}
}
findImportedRoomId(importId: string): string | null {
if (this._roomCache.has(importId)) {
return this._roomCache.get(importId) as string;
}
const options = {
fields: {
_id: 1,
},
};
const room = Rooms.findOneByImportId(importId, options);
if (room) {
return this.addRoomToCache(importId, room._id);
}
return null;
}
findImportedRoomName(importId: string): string | undefined {
if (this._roomNameCache.has(importId)) {
return this._roomNameCache.get(importId) as string;
}
const options = {
fields: {
_id: 1,
name: 1,
},
};
const room = Rooms.findOneByImportId(importId, options);
if (room) {
if (!this._roomCache.has(importId)) {
this.addRoomToCache(importId, room._id);
}
return this.addRoomNameToCache(importId, room.name);
}
}
findImportedUser(importId: string): IUserIdentification | null {
const options = {
fields: {
_id: 1,
username: 1,
},
};
if (importId === 'rocket.cat') {
return {
_id: 'rocket.cat',
username: 'rocket.cat',
};
}
if (this._userCache.has(importId)) {
return this._userCache.get(importId) as IUserIdentification;
}
const user = Users.findOneByImportId(importId, options);
if (user) {
return this.addUserToCache(importId, user._id, user.username);
}
return null;
}
findImportedUserId(_id: string): string | undefined {
const data = this.findImportedUser(_id);
return data?._id;
}
findImportedUsername(_id: string): string | undefined {
const data = this.findImportedUser(_id);
return data?.username;
}
findImportedUserDisplayName(importId: string): string | undefined {
const options = {
fields: {
_id: 1,
name: 1,
username: 1,
},
};
if (this._userDisplayNameCache.has(importId)) {
return this._userDisplayNameCache.get(importId);
}
const user = importId === 'rocket.cat' ? Users.findOneById('rocket.cat', options) : Users.findOneByImportId(importId, options);
if (user) {
if (!this._userCache.has(importId)) {
this.addUserToCache(importId, user._id, user.username);
}
return this.addUserDisplayNameToCache(importId, user.name);
}
}
updateRoomId(_id: string, roomData: IImportChannel): void {
const set = {
ts: roomData.ts,
topic: roomData.topic,
description: roomData.description,
};
const roomUpdate: {$set?: Record<string, any>; $addToSet?: Record<string, any>} = {};
if (Object.keys(set).length > 0) {
roomUpdate.$set = set;
}
if (roomData.importIds.length) {
roomUpdate.$addToSet = {
importIds: {
$each: roomData.importIds,
},
};
}
if (roomUpdate.$set || roomUpdate.$addToSet) {
Rooms.update({ _id: roomData._id }, roomUpdate);
}
}
getRoomCreatorId(roomData: IImportChannel, startedByUserId: string): string {
if (roomData.u) {
const creatorId = this.findImportedUserId(roomData.u._id);
if (creatorId) {
return creatorId;
}
if (roomData.t !== 'd') {
return startedByUserId;
}
throw new Error('importer-channel-invalid-creator');
}
if (roomData.t === 'd') {
for (const member of roomData.users) {
const userId = this.findImportedUserId(member);
if (userId) {
return userId;
}
}
}
throw new Error('importer-channel-invalid-creator');
}
insertRoom(roomData: IImportChannel, startedByUserId: string): void {
// Find the rocketchatId of the user who created this channel
const creatorId = this.getRoomCreatorId(roomData, startedByUserId);
const members = this.convertImportedIdsToUsernames(roomData.users, roomData.t !== 'd' ? creatorId : undefined);
if (roomData.t === 'd') {
if (members.length < roomData.users.length) {
console.warn('One or more imported users not found: ${ roomData.users }');
throw new Error('importer-channel-missing-users');
}
}
// Create the channel
try {
Meteor.runAsUser(creatorId, () => {
const roomInfo = roomData.t === 'd'
? Meteor.call('createDirectMessage', ...members)
: Meteor.call(roomData.t === 'p' ? 'createPrivateGroup' : 'createChannel', roomData.name, members);
roomData._id = roomInfo.rid;
});
} catch (e) {
console.warn(roomData.name, members);
console.error(e);
throw e;
}
this.updateRoomId(roomData._id as 'string', roomData);
}
convertImportedIdsToUsernames(importedIds: Array<string>, idToRemove: string | undefined = undefined): Array<string> {
return importedIds.map((user) => {
if (user === 'rocket.cat') {
return user;
}
if (this._userCache.has(user)) {
const cache = this._userCache.get(user);
if (cache) {
return cache.username;
}
}
const obj = Users.findOneByImportId(user, { fields: { _id: 1, username: 1 } });
if (obj) {
this.addUserToCache(user, obj._id, obj.username);
if (idToRemove && obj._id === idToRemove) {
return false;
}
return obj.username;
}
return false;
}).filter((user) => user);
}
findExistingRoom(data: IImportChannel): IRoom {
if (data._id && data._id.toUpperCase() === 'GENERAL') {
const room = Rooms.findOneById('GENERAL', {});
// Prevent the importer from trying to create a new general
if (!room) {
throw new Error('importer-channel-general-not-found');
}
return room;
}
if (data.t === 'd') {
const users = this.convertImportedIdsToUsernames(data.users);
if (users.length !== data.users.length) {
throw new Error('importer-channel-missing-users');
}
return Rooms.findDirectRoomContainingAllUsernames(users, {});
}
return Rooms.findOneByNonValidatedName(data.name, {});
}
convertChannels(startedByUserId: string, { beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
const channels = ImportData.find({ dataType: 'channel' });
channels.forEach(({ data: c, _id }: IImportChannelRecord) => {
try {
if (beforeImportFn && !beforeImportFn(c, 'channel')) {
this.skipRecord(_id);
return;
}
if (!c.name && c.t !== 'd') {
throw new Error('importer-channel-missing-name');
}
c.importIds = c.importIds.filter((item) => item);
c.users = _.uniq(c.users);
if (!c.importIds.length) {
throw new Error('importer-channel-missing-import-id');
}
const existingRoom = this.findExistingRoom(c);
if (existingRoom) {
this.updateRoom(existingRoom, c, startedByUserId);
} else {
this.insertRoom(c, startedByUserId);
}
if (c.archived && c._id) {
this.archiveRoomById(c._id);
}
if (afterImportFn) {
afterImportFn(c, 'channel');
}
} catch (e) {
this.saveError(_id, e);
}
});
}
archiveRoomById(rid: string): void {
Rooms.archiveById(rid);
Subscriptions.archiveByRoomId(rid);
}
convertData(startedByUserId: string, callbacks: IConversionCallbacks = {}): void {
this.convertUsers(callbacks);
this.convertChannels(startedByUserId, callbacks);
this.convertMessages(callbacks);
Meteor.defer(() => {
this.clearSuccessfullyImportedData();
});
}
clearImportData(): void {
const rawCollection = ImportData.model.rawCollection();
const remove = Meteor.wrapAsync(rawCollection.remove, rawCollection);
remove({});
}
clearSuccessfullyImportedData(): void {
ImportData.model.rawCollection().remove({
errors: {
$exists: false,
},
});
}
}

@ -7,67 +7,25 @@ import AdmZip from 'adm-zip';
import getFileType from 'file-type';
import { Progress } from './ImporterProgress';
import { Selection } from './ImporterSelection';
import { ImporterWebsocket } from './ImporterWebsocket';
import { ProgressStep } from '../../lib/ImporterProgressStep';
import { ImporterInfo } from '../../lib/ImporterInfo';
import { RawImports } from '../models/RawImports';
import { Settings, Imports } from '../../../models';
import { Logger } from '../../../logger';
import { FileUpload } from '../../../file-upload';
import { sendMessage } from '../../../lib';
import { ImportDataConverter } from './ImportDataConverter';
import { ImportData } from '../models/ImportData';
import { t } from '../../../utils/server';
import {
Selection,
SelectionChannel,
SelectionUser,
} from '..';
/**
* Base class for all of the importers.
*/
export class Base {
/**
* The max BSON object size we can store in MongoDB is 16777216 bytes
* but for some reason the mongo instanace which comes with Meteor
* errors out for anything close to that size. So, we are rounding it
* down to 8000000 bytes.
*
* @param {any} item The item to calculate the BSON size of.
* @returns {number} The size of the item passed in.
* @static
*/
static getBSONSize(item) {
const { calculateObjectSize } = require('bson');
return calculateObjectSize(item);
}
/**
* The max BSON object size we can store in MongoDB is 16777216 bytes
* but for some reason the mongo instanace which comes with Meteor
* errors out for anything close to that size. So, we are rounding it
* down to 6000000 bytes.
*
* @returns {number} 8000000 bytes.
*/
static getMaxBSONSize() {
return 6000000;
}
/**
* Splits the passed in array to at least one array which has a size that
* is safe to store in the database.
*
* @param {any[]} theArray The array to split out
* @returns {any[][]} The safe sized arrays
* @static
*/
static getBSONSafeArraysFromAnArray(theArray) {
const BSONSize = Base.getBSONSize(theArray);
const maxSize = Math.floor(theArray.length / Math.ceil(BSONSize / Base.getMaxBSONSize()));
const safeArrays = [];
let i = 0;
while (i < theArray.length) {
safeArrays.push(theArray.slice(i, i += maxSize));
}
return safeArrays;
}
/**
* Constructs a new importer, adding an empty collection, AdmZip property, and empty users & channels
*
@ -84,6 +42,7 @@ export class Base {
this.https = https;
this.AdmZip = AdmZip;
this.getFileType = getFileType;
this.converter = new ImportDataConverter();
this.prepare = this.prepare.bind(this);
this.startImport = this.startImport.bind(this);
@ -92,7 +51,6 @@ export class Base {
this.addCountToTotal = this.addCountToTotal.bind(this);
this.addCountCompleted = this.addCountCompleted.bind(this);
this.updateRecord = this.updateRecord.bind(this);
this.uploadFile = this.uploadFile.bind(this);
this.info = info;
@ -196,7 +154,71 @@ export class Base {
throw new Error(`Channels in the selected data wasn't found, it must but at least an empty array for the ${ this.info.name } importer.`);
}
return this.updateProgress(ProgressStep.IMPORTING_STARTED);
this.updateProgress(ProgressStep.IMPORTING_STARTED);
this.reloadCount();
const started = Date.now();
const startedByUserId = Meteor.userId();
console.log(importSelection);
const beforeImportFn = (data, type) => {
switch (type) {
case 'channel': {
const id = data.t === 'd' ? '__directMessages__' : data.importIds[0];
for (const channel of importSelection.channels) {
if (channel.channel_id === id) {
return channel.do_import;
}
}
return false;
}
case 'user': {
const id = data.importIds[0];
for (const user of importSelection.users) {
if (user.user_id === id) {
return user.do_import;
}
}
return false;
}
}
return true;
};
const afterImportFn = () => {
this.addCountCompleted(1);
};
Meteor.defer(() => {
try {
this.updateProgress(ProgressStep.IMPORTING_USERS);
this.converter.convertUsers({ beforeImportFn, afterImportFn });
this.updateProgress(ProgressStep.IMPORTING_CHANNELS);
this.converter.convertChannels(startedByUserId, { beforeImportFn, afterImportFn });
this.updateProgress(ProgressStep.IMPORTING_MESSAGES);
this.converter.convertMessages({ afterImportFn });
this.updateProgress(ProgressStep.FINISHING);
Meteor.defer(() => {
this.converter.clearSuccessfullyImportedData();
});
this.updateProgress(ProgressStep.DONE);
} catch (e) {
this.logger.error(e);
this.updateProgress(ProgressStep.ERROR);
}
const timeTook = Date.now() - started;
this.logger.log(`Import took ${ timeTook } milliseconds.`);
});
return this.getProgress();
}
/**
@ -352,18 +374,6 @@ export class Base {
});
}
flagConflictingEmails(emailList) {
Imports.model.update({
_id: this.importRecord._id,
'fileData.users.email': { $in: emailList },
}, {
$set: {
'fileData.users.$.is_email_taken': true,
'fileData.users.$.do_import': false,
},
});
}
/**
* Updates the import record with the given fields being `set`.
*
@ -377,79 +387,23 @@ export class Base {
return this.importRecord;
}
/**
* Uploads the file to the storage.
*
* @param {any} details An object with details about the upload: `name`, `size`, `type`, and `rid`.
* @param {string} fileUrl Url of the file to download/import.
* @param {any} user The Rocket.Chat user.
* @param {any} room The Rocket.Chat Room.
* @param {Date} timeStamp The timestamp the file was uploaded
*/
uploadFile(details, fileUrl, user, room, timeStamp) {
this.logger.debug(`Uploading the file ${ details.name } from ${ fileUrl }.`);
const requestModule = /https/i.test(fileUrl) ? this.https : this.http;
const fileStore = FileUpload.getStore('Uploads');
return requestModule.get(fileUrl, Meteor.bindEnvironment(function(res) {
const contentType = res.headers['content-type'];
if (!details.type && contentType) {
details.type = contentType;
}
buildSelection() {
this.updateProgress(ProgressStep.USER_SELECTION);
const rawData = [];
res.on('data', (chunk) => rawData.push(chunk));
res.on('end', Meteor.bindEnvironment(() => {
fileStore.insert(details, Buffer.concat(rawData), function(err, file) {
if (err) {
throw new Error(err);
} else {
const url = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`);
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 != null ? file.identify.size : undefined;
}
const users = ImportData.getAllUsersForSelection();
const channels = ImportData.getAllChannelsForSelection();
const hasDM = ImportData.checkIfDirectMessagesExists();
if (/^audio\/.+/.test(file.type)) {
attachment.audio_url = url;
attachment.audio_type = file.type;
attachment.audio_size = file.size;
}
const selectionUsers = users.map((u) => new SelectionUser(u.data.importIds[0], u.data.username, u.data.emails[0], Boolean(u.data.deleted), u.data.type === 'bot', true));
const selectionChannels = channels.map((c) => new SelectionChannel(c.data.importIds[0], c.data.name, Boolean(c.data.archived), true, c.data.t === 'p', undefined, c.data.t === 'd'));
const selectionMessages = ImportData.countMessages();
if (/^video\/.+/.test(file.type)) {
attachment.video_url = url;
attachment.video_type = file.type;
attachment.video_size = file.size;
}
if (hasDM) {
selectionChannels.push(new SelectionChannel('__directMessages__', t('Direct_Messages'), false, true, true, undefined, true));
}
const msg = {
rid: details.rid,
ts: timeStamp,
msg: '',
file: {
_id: file._id,
},
groupable: false,
attachments: [attachment],
};
if ((details.message_id != null) && (typeof details.message_id === 'string')) {
msg._id = details.message_id;
}
const results = new Selection(this.name, selectionUsers, selectionChannels, selectionMessages);
return sendMessage(user, msg, room, true);
}
});
}));
}));
return results;
}
}

@ -0,0 +1,14 @@
export interface IImportChannel {
_id?: string;
u?: {
_id: string;
};
name?: string;
users: Array<string>;
importIds: Array<string>;
t: string;
topic?: string;
description?: string;
ts?: Date;
archived?: boolean;
}

@ -0,0 +1,53 @@
export type IImportedId = 'string';
export interface IImportMessageReaction {
name: string;
users: Array<IImportedId>;
}
export interface IImportPendingFile {
downloadUrl: string;
id: string;
size: number;
name: string;
external: boolean;
source: string;
original: Record<string, any>;
}
export interface IImportAttachment extends Record<string, any> {
text: string;
title: string;
fallback: string;
}
export interface IImportMessage {
_id?: IImportedId;
rid: IImportedId;
u: {
_id: IImportedId;
};
msg: string;
alias?: string;
ts: Date;
t?: string;
reactions?: Record<string, IImportMessageReaction>;
groupable?: boolean;
tmid?: IImportedId;
tlm?: Date;
tcount?: number;
replies?: Array<IImportedId>;
editedAt?: Date;
editedBy?: IImportedId;
mentions?: Array<IImportedId>;
channels?: Array<string>;
attachments?: IImportAttachment;
bot?: boolean;
emoji?: string;
url?: string;
_importFile?: IImportPendingFile;
}

@ -0,0 +1,28 @@
import { IImportUser } from './IImportUser';
import { IImportChannel } from './IImportChannel';
import { IImportMessage } from './IImportMessage';
export interface IImportRecord {
data: IImportUser | IImportChannel | IImportMessage;
dataType: 'user' | 'channel' | 'message';
_id: string;
options?: {};
}
export interface IImportUserRecord extends IImportRecord {
data: IImportUser;
dataType: 'user';
}
export interface IImportChannelRecord extends IImportRecord {
data: IImportChannel;
dataType: 'channel';
}
export interface IImportMessageRecord extends IImportRecord {
data: IImportMessage;
dataType: 'message';
options: {
useQuickInsert?: boolean;
};
}

@ -0,0 +1,17 @@
export interface IImportUser {
// #ToDo: Remove this _id, as it isn't part of the imported data
_id?: string;
username?: string;
emails: Array<string>;
importIds: Array<string>;
name?: string;
utcOffset?: number;
active?: boolean;
avatarUrl?: string;
deleted?: boolean;
statusText?: string;
roles?: Array<string>;
type: 'user' | 'bot';
bio?: string;
}

@ -2,6 +2,7 @@ import { Base } from './classes/ImporterBase';
import { ImporterWebsocket } from './classes/ImporterWebsocket';
import { Progress } from './classes/ImporterProgress';
import { RawImports } from './models/RawImports';
import { ImportData } from './models/ImportData';
import { Selection } from './classes/ImporterSelection';
import { SelectionChannel } from './classes/ImporterSelectionChannel';
import { SelectionUser } from './classes/ImporterSelectionUser';
@ -25,6 +26,7 @@ export {
Progress,
ProgressStep,
RawImports,
ImportData,
Selection,
SelectionChannel,
SelectionUser,

@ -58,31 +58,17 @@ Meteor.methods({
];
if (readySteps.indexOf(importer.instance.progress.step) >= 0) {
if (importer.instance.importRecord && importer.instance.importRecord.fileData) {
return importer.instance.importRecord.fileData;
}
return importer.instance.buildSelection();
}
const fileName = importer.instance.importRecord.file;
const fullFilePath = fs.existsSync(fileName) ? fileName : path.join(RocketChatImportFileInstance.absolutePath, fileName);
const results = importer.instance.prepareUsingLocalFile(fullFilePath);
if (results instanceof Promise) {
return results.then((data) => {
importer.instance.updateRecord({
fileData: data,
});
return data;
}).catch((e) => {
console.error(e);
throw new Meteor.Error(e);
});
const promise = importer.instance.prepareUsingLocalFile(fullFilePath);
if (promise && promise instanceof Promise) {
Promise.await(promise);
}
importer.instance.updateRecord({
fileData: results,
});
return results;
return importer.instance.buildSelection();
},
});

@ -0,0 +1,88 @@
import { Base } from '../../../models/server';
import { IImportUserRecord, IImportChannelRecord } from '../definitions/IImportRecord';
class ImportDataModel extends Base {
constructor() {
super('import_data');
}
getAllUsersForSelection(): Array<IImportUserRecord> {
return this.find({
dataType: 'user',
}, {
fields: {
'data.importIds': 1,
'data.username': 1,
'data.emails': 1,
'data.deleted': 1,
'data.type': 1,
},
}).fetch();
}
getAllChannelsForSelection(): Array<IImportChannelRecord> {
return this.find({
dataType: 'channel',
'data.t': {
$ne: 'd',
},
}, {
fields: {
'data.importIds': 1,
'data.name': 1,
'data.archived': 1,
'data.t': 1,
},
}).fetch();
}
checkIfDirectMessagesExists(): boolean {
return this.find({
dataType: 'channel',
'data.t': 'd',
}, {
fields: {
_id: 1,
},
}).count() > 0;
}
countMessages(): number {
return this.find({
dataType: 'message',
}).count();
}
findChannelImportIdByNameOrImportId(channelIdentifier: string): string | undefined {
const channel = this.findOne({
dataType: 'channel',
$or: [
{
'data.name': channelIdentifier,
},
{
'data.importIds': channelIdentifier,
},
],
}, {
fields: {
'data.importIds': 1,
},
});
return channel?.data?.importIds?.shift();
}
findDMForImportedUsers(...users: Array<string>): IImportChannelRecord | undefined {
const query = {
dataType: 'channel',
'data.users': {
$all: users,
},
};
return this.findOne(query);
}
}
export const ImportData = new ImportDataModel();

@ -8,7 +8,7 @@ function runDrop(fn) {
try {
fn();
} catch (e) {
console.log('errror', e); // TODO: Remove
console.log('error', e); // TODO: Remove
// ignored
}
}
@ -21,9 +21,7 @@ Meteor.startup(function() {
// And there's still data for it on the temp collection
// Then we can keep the data there to let the user try again
if (lastOperation && [ProgressStep.USER_SELECTION, ProgressStep.ERROR].includes(lastOperation.status)) {
if (RawImports.find({ import: lastOperation._id }).count() > 0) {
idToKeep = lastOperation._id;
}
idToKeep = lastOperation._id;
}
if (idToKeep) {

@ -76,8 +76,8 @@ const validateAttachment = (attachment) => {
const validateBodyAttachments = (attachments) => attachments.map(validateAttachment);
export const insertMessage = function(user, message, room, upsert = false) {
if (!user || !message || !room._id) {
export const insertMessage = function(user, message, rid, upsert = false) {
if (!user || !message || !rid) {
return false;
}
@ -103,7 +103,7 @@ export const insertMessage = function(user, message, room, upsert = false) {
_id,
username,
};
message.rid = room._id;
message.rid = rid;
if (!Match.test(message.msg, String)) {
message.msg = '';

@ -939,7 +939,7 @@ export class Rooms extends Base {
return this.update(query, update);
}
resetLastMessageById(_id, messageId) {
resetLastMessageById(_id, messageId = undefined) {
const query = { _id };
const lastMessage = Messages.getLastVisibleMessageSentWithNoTypeByRoomId(_id, messageId);

@ -21,7 +21,6 @@ import '../app/google-vision/client';
import '../app/iframe-login/client';
import '../app/importer/client';
import '../app/importer-csv/client';
import '../app/importer-hipchat/client';
import '../app/importer-hipchat-enterprise/client';
import '../app/importer-slack/client';
import '../app/importer-slack-users/client';

@ -58,11 +58,8 @@ function ImportHistoryPage() {
t,
]);
const hasAnySuccessfulSlackImport = useMemo(
() =>
latestOperations?.some(
({ importerKey, status }) => importerKey === 'slack' && status === ProgressStep.DONE,
),
const hasAnySuccessfulImport = useMemo(
() => latestOperations?.some(({ status }) => status === ProgressStep.DONE),
[latestOperations],
);
@ -119,12 +116,12 @@ function ImportHistoryPage() {
<Button primary disabled={isLoading} onClick={handleNewImportClick}>
{t('Import_New_File')}
</Button>
{hasAnySuccessfulSlackImport && (
{hasAnySuccessfulImport && (
<Button disabled={isLoading} onClick={handleDownloadPendingFilesClick}>
{t('Download_Pending_Files')}
</Button>
)}
{hasAnySuccessfulSlackImport && (
{hasAnySuccessfulImport && (
<Button disabled={isLoading} onClick={handleDownloadPendingAvatarsClick}>
{t('Download_Pending_Avatars')}
</Button>

@ -31,7 +31,6 @@ import '../app/google-vision/server';
import '../app/iframe-login/server';
import '../app/importer/server';
import '../app/importer-csv/server';
import '../app/importer-hipchat/server';
import '../app/importer-hipchat-enterprise/server';
import '../app/importer-pending-files/server';
import '../app/importer-pending-avatars/server';

Loading…
Cancel
Save