You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
934 lines
24 KiB
934 lines
24 KiB
import { Meteor } from 'meteor/meteor';
|
|
import { Accounts } from 'meteor/accounts-base';
|
|
import _ from 'underscore';
|
|
import { ObjectId } from 'mongodb';
|
|
|
|
import { ImportData as ImportDataRaw } from '../../../models/server/raw';
|
|
import { IImportUser } from '../../../../definition/IImportUser';
|
|
import { IImportMessage, IImportMessageReaction } from '../../../../definition/IImportMessage';
|
|
import { IImportChannel } from '../../../../definition/IImportChannel';
|
|
import { IConversionCallbacks } from '../definitions/IConversionCallbacks';
|
|
import { IImportUserRecord, IImportChannelRecord, IImportMessageRecord } from '../../../../definition/IImportRecord';
|
|
import { Users, Rooms, Subscriptions, ImportData } from '../../../models/server';
|
|
import { generateUsernameSuggestion, insertMessage, saveUserIdentity, addUserToDefaultChannels } from '../../../lib/server';
|
|
import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus';
|
|
import { IUser, IUserEmail } from '../../../../definition/IUser';
|
|
import type { Logger } from '../../../../server/lib/logger/Logger';
|
|
|
|
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>;
|
|
|
|
export type IConverterOptions = {
|
|
flagEmailsAsVerified?: boolean;
|
|
skipExistingUsers?: boolean;
|
|
};
|
|
|
|
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>;
|
|
|
|
private _logger: Logger;
|
|
|
|
private _options: IConverterOptions;
|
|
|
|
public get options(): IConverterOptions {
|
|
return this._options;
|
|
}
|
|
|
|
constructor(options?: IConverterOptions) {
|
|
this._options = options || {
|
|
flagEmailsAsVerified: false,
|
|
skipExistingUsers: false,
|
|
};
|
|
this._userCache = new Map();
|
|
this._userDisplayNameCache = new Map();
|
|
this._roomCache = new Map();
|
|
this._roomNameCache = new Map();
|
|
}
|
|
|
|
setLogger(logger: Logger): void {
|
|
this._logger = logger;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
protected addObject(type: string, data: Record<string, any>, options: Record<string, any> = {}): void {
|
|
ImportData.model.rawCollection().insert({
|
|
_id: new ObjectId().toHexString(),
|
|
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,
|
|
});
|
|
}
|
|
|
|
addUserImportId(updateData: Record<string, any>, userData: IImportUser): void {
|
|
if (userData.importIds?.length) {
|
|
updateData.$addToSet = {
|
|
importIds: {
|
|
$each: userData.importIds,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
addUserEmails(updateData: Record<string, any>, userData: IImportUser, existingEmails: Array<IUserEmail>): void {
|
|
if (!userData.emails?.length) {
|
|
return;
|
|
}
|
|
|
|
const verifyEmails = Boolean(this.options.flagEmailsAsVerified);
|
|
const newEmailList: Array<IUserEmail> = [];
|
|
|
|
for (const email of userData.emails) {
|
|
const verified = verifyEmails || existingEmails.find((ee) => ee.address === email)?.verified || false;
|
|
|
|
newEmailList.push({
|
|
address: email,
|
|
verified,
|
|
});
|
|
}
|
|
|
|
updateData.$set.emails = newEmailList;
|
|
}
|
|
|
|
addUserServices(updateData: Record<string, any>, userData: IImportUser): void {
|
|
if (!userData.services) {
|
|
return;
|
|
}
|
|
|
|
for (const serviceKey in userData.services) {
|
|
if (!userData.services[serviceKey]) {
|
|
continue;
|
|
}
|
|
|
|
const service = userData.services[serviceKey];
|
|
|
|
for (const key in service) {
|
|
if (!service[key]) {
|
|
continue;
|
|
}
|
|
|
|
updateData.$set[`services.${serviceKey}.${key}`] = service[key];
|
|
}
|
|
}
|
|
}
|
|
|
|
addCustomFields(updateData: Record<string, any>, userData: IImportUser): void {
|
|
if (!userData.customFields) {
|
|
return;
|
|
}
|
|
|
|
const subset = (source: Record<string, any>, currentPath: string): void => {
|
|
for (const key in source) {
|
|
if (!source.hasOwnProperty(key)) {
|
|
continue;
|
|
}
|
|
|
|
const keyPath = `${currentPath}.${key}`;
|
|
if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
subset(source[key], keyPath);
|
|
continue;
|
|
}
|
|
|
|
updateData.$set[keyPath] = source[key];
|
|
}
|
|
};
|
|
|
|
subset(userData.customFields, 'customFields');
|
|
}
|
|
|
|
updateUser(existingUser: IUser, userData: IImportUser): void {
|
|
const { _id } = existingUser;
|
|
|
|
userData._id = _id;
|
|
|
|
if (!userData.roles && !existingUser.roles) {
|
|
userData.roles = ['user'];
|
|
}
|
|
if (!userData.type && !existingUser.type) {
|
|
userData.type = 'user';
|
|
}
|
|
|
|
// #ToDo: #TODO: Move this to the model class
|
|
const updateData: Record<string, any> = {
|
|
$set: {
|
|
...(userData.roles && { roles: userData.roles }),
|
|
...(userData.type && { type: userData.type }),
|
|
...(userData.statusText && { statusText: userData.statusText }),
|
|
...(userData.bio && { bio: userData.bio }),
|
|
...(userData.services?.ldap && { ldap: true }),
|
|
...(userData.avatarUrl && { _pendingAvatarUrl: userData.avatarUrl }),
|
|
},
|
|
};
|
|
|
|
this.addCustomFields(updateData, userData);
|
|
this.addUserServices(updateData, userData);
|
|
this.addUserImportId(updateData, userData);
|
|
this.addUserEmails(updateData, userData, existingUser.emails || []);
|
|
|
|
if (Object.keys(updateData.$set).length === 0) {
|
|
delete updateData.$set;
|
|
}
|
|
if (Object.keys(updateData).length > 0) {
|
|
Users.update({ _id }, updateData);
|
|
}
|
|
|
|
if (userData.utcOffset) {
|
|
Users.setUtcOffset(_id, userData.utcOffset);
|
|
}
|
|
|
|
if (userData.name || userData.username) {
|
|
saveUserIdentity({ _id, name: userData.name, username: userData.username });
|
|
}
|
|
|
|
if (userData.importIds.length) {
|
|
this.addUserToCache(userData.importIds[0], existingUser._id, existingUser.username);
|
|
}
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
const user = Users.findOneById(userId, {});
|
|
this.updateUser(user, userData);
|
|
|
|
addUserToDefaultChannels(user, true);
|
|
return user;
|
|
}
|
|
|
|
protected async getUsersToImport(): Promise<Array<IImportUserRecord>> {
|
|
return ImportDataRaw.getAllUsers().toArray();
|
|
}
|
|
|
|
findExistingUser(data: IImportUser): IUser | undefined {
|
|
if (data.emails.length) {
|
|
const emailUser = Users.findOneByEmailAddress(data.emails[0], {});
|
|
|
|
if (emailUser) {
|
|
return emailUser;
|
|
}
|
|
}
|
|
|
|
// If we couldn't find one by their email address, try to find an existing user by their username
|
|
if (data.username) {
|
|
return Users.findOneByUsernameIgnoringCase(data.username, {});
|
|
}
|
|
}
|
|
|
|
public convertUsers({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
|
|
const users = Promise.await(this.getUsersToImport());
|
|
users.forEach(({ data, _id }) => {
|
|
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 = this.findExistingUser(data);
|
|
if (existingUser && this._options.skipExistingUsers) {
|
|
this.skipRecord(_id);
|
|
return;
|
|
}
|
|
|
|
if (!data.username) {
|
|
data.username = generateUsernameSuggestion({
|
|
name: data.name,
|
|
emails: data.emails,
|
|
});
|
|
}
|
|
|
|
const isNewUser = !existingUser;
|
|
|
|
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);
|
|
} else if (data.deleted === false && existingUser?.active === false) {
|
|
setUserActiveStatus(data._id, true);
|
|
}
|
|
|
|
if (afterImportFn) {
|
|
afterImportFn(data, 'user', isNewUser);
|
|
}
|
|
} catch (e) {
|
|
this._logger.error(e);
|
|
this.saveError(_id, e);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected saveError(importId: string, error: Error): void {
|
|
this._logger.error(error);
|
|
ImportData.update(
|
|
{
|
|
_id: importId,
|
|
},
|
|
{
|
|
$push: {
|
|
errors: {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
protected 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) {
|
|
this._logger.warn(`Mentioned room not found: ${importId}`);
|
|
continue;
|
|
}
|
|
|
|
message.msg = message.msg.replace(new RegExp(`\#${importId}`, 'gi'), `#${name}`);
|
|
|
|
result.push({
|
|
_id,
|
|
name,
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
protected async getMessagesToImport(): Promise<Array<IImportMessageRecord>> {
|
|
return ImportDataRaw.getAllMessages().toArray();
|
|
}
|
|
|
|
convertMessages({ beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
|
|
const rids: Array<string> = [];
|
|
const messages = Promise.await(this.getMessagesToImport());
|
|
messages.forEach(({ data, _id }: IImportMessageRecord) => {
|
|
try {
|
|
if (beforeImportFn && !beforeImportFn(data, 'message')) {
|
|
this.skipRecord(_id);
|
|
return;
|
|
}
|
|
|
|
if (!data.ts || isNaN(data.ts as unknown as number)) {
|
|
throw new Error('importer-message-invalid-timestamp');
|
|
}
|
|
|
|
const creator = this.findImportedUser(data.u._id);
|
|
if (!creator) {
|
|
this._logger.warn(`Imported user not found: ${data.u._id}`);
|
|
throw new Error('importer-message-unknown-user');
|
|
}
|
|
|
|
const rid = this.findImportedRoomId(data.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 = data.mentions && this.convertMessageMentions(data);
|
|
const channels = data.channels && this.convertMessageChannels(data);
|
|
|
|
const msgObj: IMessage = {
|
|
rid,
|
|
u: {
|
|
_id: creator._id,
|
|
username: creator.username,
|
|
},
|
|
msg: data.msg,
|
|
ts: data.ts,
|
|
t: data.t || undefined,
|
|
groupable: data.groupable,
|
|
tmid: data.tmid,
|
|
tlm: data.tlm,
|
|
tcount: data.tcount,
|
|
replies: data.replies && this.convertMessageReplies(data.replies),
|
|
editedAt: data.editedAt,
|
|
editedBy: data.editedBy && (this.findImportedUser(data.editedBy) || undefined),
|
|
mentions,
|
|
channels,
|
|
_importFile: data._importFile,
|
|
url: data.url,
|
|
attachments: data.attachments,
|
|
bot: data.bot,
|
|
emoji: data.emoji,
|
|
alias: data.alias,
|
|
};
|
|
|
|
if (data._id) {
|
|
msgObj._id = data._id;
|
|
}
|
|
|
|
if (data.reactions) {
|
|
msgObj.reactions = this.convertMessageReactions(data.reactions);
|
|
}
|
|
|
|
try {
|
|
insertMessage(creator, msgObj, rid, true);
|
|
} catch (e) {
|
|
this._logger.warn(`Failed to import message with timestamp ${String(msgObj.ts)} to room ${rid}`);
|
|
this._logger.error(e);
|
|
}
|
|
|
|
if (afterImportFn) {
|
|
afterImportFn(data, 'message', true);
|
|
}
|
|
} catch (e) {
|
|
this.saveError(_id, e);
|
|
}
|
|
});
|
|
|
|
for (const rid of rids) {
|
|
try {
|
|
Rooms.resetLastMessageById(rid);
|
|
} catch (e) {
|
|
this._logger.warn(`Failed to update last message of room ${rid}`);
|
|
this._logger.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);
|
|
}
|
|
|
|
public 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) {
|
|
this._logger.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) {
|
|
this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members });
|
|
this._logger.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, {});
|
|
}
|
|
|
|
protected async getChannelsToImport(): Promise<Array<IImportChannelRecord>> {
|
|
return ImportDataRaw.getAllChannels().toArray();
|
|
}
|
|
|
|
convertChannels(startedByUserId: string, { beforeImportFn, afterImportFn }: IConversionCallbacks = {}): void {
|
|
const channels = Promise.await(this.getChannelsToImport());
|
|
channels.forEach(({ data, _id }: IImportChannelRecord) => {
|
|
try {
|
|
if (beforeImportFn && !beforeImportFn(data, 'channel')) {
|
|
this.skipRecord(_id);
|
|
return;
|
|
}
|
|
|
|
if (!data.name && data.t !== 'd') {
|
|
throw new Error('importer-channel-missing-name');
|
|
}
|
|
|
|
data.importIds = data.importIds.filter((item) => item);
|
|
data.users = _.uniq(data.users);
|
|
|
|
if (!data.importIds.length) {
|
|
throw new Error('importer-channel-missing-import-id');
|
|
}
|
|
|
|
const existingRoom = this.findExistingRoom(data);
|
|
|
|
if (existingRoom) {
|
|
this.updateRoom(existingRoom, data, startedByUserId);
|
|
} else {
|
|
this.insertRoom(data, startedByUserId);
|
|
}
|
|
|
|
if (data.archived && data._id) {
|
|
this.archiveRoomById(data._id);
|
|
}
|
|
|
|
if (afterImportFn) {
|
|
afterImportFn(data, 'channel', !existingRoom);
|
|
}
|
|
} 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();
|
|
});
|
|
}
|
|
|
|
public clearImportData(): void {
|
|
// Using raw collection since its faster
|
|
Promise.await(ImportData.model.rawCollection().remove({}));
|
|
}
|
|
|
|
clearSuccessfullyImportedData(): void {
|
|
ImportData.model.rawCollection().remove({
|
|
errors: {
|
|
$exists: false,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|