diff --git a/.eslintrc b/.eslintrc index bbae28a07d60..9445d90e05c7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,7 +1,10 @@ { "parserOptions": { "sourceType": "module", - "ecmaVersion": 2017 + "ecmaVersion": 2017, + "ecmaFeatures": { + "experimentalObjectRestSpread" : true, + } }, "env": { "browser": true, diff --git a/packages/rocketchat-importer-slack/main.coffee b/packages/rocketchat-importer-slack/main.coffee deleted file mode 100644 index da0518e53874..000000000000 --- a/packages/rocketchat-importer-slack/main.coffee +++ /dev/null @@ -1,3 +0,0 @@ -Importer.addImporter 'slack', Importer.Slack, - name: 'Slack' - mimeType: 'application/zip' diff --git a/packages/rocketchat-importer-slack/main.js b/packages/rocketchat-importer-slack/main.js new file mode 100644 index 000000000000..d7298a2700e1 --- /dev/null +++ b/packages/rocketchat-importer-slack/main.js @@ -0,0 +1,5 @@ +/* globals Importer */ +Importer.addImporter('slack', Importer.Slack, { + name: 'Slack', + mimeType: 'application/zip' +}); diff --git a/packages/rocketchat-importer-slack/package.js b/packages/rocketchat-importer-slack/package.js index e10bc401cca1..04eb0571ac4a 100644 --- a/packages/rocketchat-importer-slack/package.js +++ b/packages/rocketchat-importer-slack/package.js @@ -8,11 +8,10 @@ Package.describe({ Package.onUse(function(api) { api.use([ 'ecmascript', - 'coffeescript', 'rocketchat:lib', 'rocketchat:importer' ]); api.use('rocketchat:logger', 'server'); - api.addFiles('server.coffee', 'server'); - api.addFiles('main.coffee', ['client', 'server']); + api.addFiles('server.js', 'server'); + api.addFiles('main.js', ['client', 'server']); }); diff --git a/packages/rocketchat-importer-slack/server.coffee b/packages/rocketchat-importer-slack/server.coffee deleted file mode 100644 index bbfa2a050474..000000000000 --- a/packages/rocketchat-importer-slack/server.coffee +++ /dev/null @@ -1,373 +0,0 @@ -Importer.Slack = class Importer.Slack extends Importer.Base - constructor: (name, descriptionI18N, mimeType) -> - super(name, descriptionI18N, mimeType) - @userTags = [] - @bots = {} - @logger.debug('Constructed a new Slack Importer.') - - prepare: (dataURI, sentContentType, fileName) => - super(dataURI, sentContentType, fileName) - - {image, contentType} = RocketChatFile.dataURIParse dataURI - zip = new @AdmZip(new Buffer(image, 'base64')) - zipEntries = zip.getEntries() - - tempChannels = [] - tempUsers = [] - tempMessages = {} - for entry in zipEntries - do (entry) => - if entry.entryName.indexOf('__MACOSX') > -1 - #ignore all of the files inside of __MACOSX - @logger.debug("Ignoring the file: #{entry.entryName}") - else if entry.entryName == 'channels.json' - @updateProgress Importer.ProgressStep.PREPARING_CHANNELS - tempChannels = JSON.parse entry.getData().toString() - tempChannels = tempChannels.filter (channel) -> channel.creator? - else if entry.entryName == 'users.json' - @updateProgress Importer.ProgressStep.PREPARING_USERS - tempUsers = JSON.parse entry.getData().toString() - - for user in tempUsers when user.is_bot - @bots[user.profile.bot_id] = user - - else if not entry.isDirectory and entry.entryName.indexOf('/') > -1 - item = entry.entryName.split('/') #random/2015-10-04.json - channelName = item[0] #random - msgGroupData = item[1].split('.')[0] #2015-10-04 - if not tempMessages[channelName] - tempMessages[channelName] = {} - # Catch files which aren't valid JSON files, ignore them - try - tempMessages[channelName][msgGroupData] = JSON.parse entry.getData().toString() - catch - @logger.warn "#{entry.entryName} is not a valid JSON file! Unable to import it." - - # Insert the users record, eventually this might have to be split into several ones as well - # if someone tries to import a several thousands users instance - usersId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'users', 'users': tempUsers } - @users = @collection.findOne usersId - @updateRecord { 'count.users': tempUsers.length } - @addCountToTotal tempUsers.length - - # Insert the channels records. - channelsId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'channels', 'channels': tempChannels } - @channels = @collection.findOne channelsId - @updateRecord { 'count.channels': tempChannels.length } - @addCountToTotal tempChannels.length - - # Insert the messages records - @updateProgress Importer.ProgressStep.PREPARING_MESSAGES - messagesCount = 0 - for channel, messagesObj of tempMessages - do (channel, messagesObj) => - if not @messages[channel] - @messages[channel] = {} - for date, msgs of messagesObj - messagesCount += msgs.length - @updateRecord { 'messagesstatus': "#{channel}/#{date}" } - - if Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize - for splitMsg, i in Importer.Base.getBSONSafeArraysFromAnArray(msgs) - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}.#{i}", 'messages': splitMsg } - @messages[channel]["#{date}.#{i}"] = @collection.findOne messagesId - else - messagesId = @collection.insert { 'import': @importRecord._id, 'importer': @name, 'type': 'messages', 'name': "#{channel}/#{date}", 'messages': msgs } - @messages[channel][date] = @collection.findOne messagesId - - @updateRecord { 'count.messages': messagesCount, 'messagesstatus': null } - @addCountToTotal messagesCount - - if tempUsers.length is 0 or tempChannels.length is 0 or messagesCount is 0 - @logger.warn "The loaded users count #{tempUsers.length}, the loaded channels #{tempChannels.length}, and the loaded messages #{messagesCount}" - @updateProgress Importer.ProgressStep.ERROR - return @getProgress() - - selectionUsers = tempUsers.map (user) -> - return new Importer.SelectionUser user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot - selectionChannels = tempChannels.map (channel) -> - return new Importer.SelectionChannel channel.id, channel.name, channel.is_archived, true, false - - @updateProgress Importer.ProgressStep.USER_SELECTION - return new Importer.Selection @name, selectionUsers, selectionChannels - - startImport: (importSelection) => - super(importSelection) - start = Date.now() - - for user in importSelection.users - for u in @users.users when u.id is user.user_id - u.do_import = user.do_import - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - for channel in importSelection.channels - for c in @channels.channels when c.id is channel.channel_id - c.do_import = channel.do_import - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - startedByUserId = Meteor.userId() - Meteor.defer => - @updateProgress Importer.ProgressStep.IMPORTING_USERS - for user in @users.users when user.do_import - do (user) => - Meteor.runAsUser startedByUserId, () => - existantUser = RocketChat.models.Users.findOneByEmailAddress user.profile.email - if not existantUser - existantUser = RocketChat.models.Users.findOneByUsername user.name - - if existantUser - user.rocketId = existantUser._id - RocketChat.models.Users.update { _id: user.rocketId }, { $addToSet: { importIds: user.id } } - @userTags.push - slack: "<@#{user.id}>" - slackLong: "<@#{user.id}|#{user.name}>" - rocket: "@#{existantUser.username}" - else - if user.profile.email - userId = Accounts.createUser { email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() } - else - userId = Accounts.createUser { username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true } - Meteor.runAsUser userId, () => - Meteor.call 'setUsername', user.name, {joinDefaultChannelsSilenced: true} - url = null - if user.profile.image_original - url = user.profile.image_original - else if user.profile.image_512 - url = user.profile.image_512 - - try - Meteor.call 'setAvatarFromService', url, undefined, 'url' - catch error - this.logger.warn "Failed to set #{user.name}'s avatar from url #{url}" - - # Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 - if user.tz_offset - Meteor.call 'userSetUtcOffset', user.tz_offset / 3600 - - RocketChat.models.Users.update { _id: userId }, { $addToSet: { importIds: user.id } } - - if user.profile.real_name - RocketChat.models.Users.setName userId, user.profile.real_name - #Deleted users are 'inactive' users in Rocket.Chat - if user.deleted - Meteor.call 'setUserActiveStatus', userId, false - #TODO: Maybe send emails? - user.rocketId = userId - @userTags.push - slack: "<@#{user.id}>" - slackLong: "<@#{user.id}|#{user.name}>" - rocket: "@#{user.name}" - @addCountCompleted 1 - @collection.update { _id: @users._id }, { $set: { 'users': @users.users }} - - @updateProgress Importer.ProgressStep.IMPORTING_CHANNELS - for channel in @channels.channels when channel.do_import - do (channel) => - Meteor.runAsUser startedByUserId, () => - existantRoom = RocketChat.models.Rooms.findOneByName channel.name - if existantRoom or channel.is_general - if channel.is_general and channel.name isnt existantRoom?.name - Meteor.call 'saveRoomSettings', 'GENERAL', 'roomName', channel.name - channel.rocketId = if channel.is_general then 'GENERAL' else existantRoom._id - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $addToSet: { importIds: channel.id } } - else - users = [] - for member in channel.members when member isnt channel.creator - user = @getRocketUser member - if user? - users.push user.username - - userId = startedByUserId - for user in @users.users when user.id is channel.creator and user.do_import - userId = user.rocketId - - Meteor.runAsUser userId, () => - returned = Meteor.call 'createChannel', channel.name, users - channel.rocketId = returned.rid - - # @TODO implement model specific function - roomUpdate = - ts: new Date(channel.created * 1000) - - if not _.isEmpty channel.topic?.value - roomUpdate.topic = channel.topic.value - - if not _.isEmpty(channel.purpose?.value) - roomUpdate.description = channel.purpose.value - - RocketChat.models.Rooms.update { _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } } - - @addCountCompleted 1 - @collection.update { _id: @channels._id }, { $set: { 'channels': @channels.channels }} - - missedTypes = {} - ignoreTypes = { 'bot_add': true, 'file_comment': true, 'file_mention': true } - @updateProgress Importer.ProgressStep.IMPORTING_MESSAGES - for channel, messagesObj of @messages - do (channel, messagesObj) => - Meteor.runAsUser startedByUserId, () => - slackChannel = @getSlackChannelFromName channel - if slackChannel?.do_import - room = RocketChat.models.Rooms.findOneById slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } } - for date, msgs of messagesObj - @updateRecord { 'messagesstatus': "#{channel}/#{date}.#{msgs.messages.length}" } - for message in msgs.messages - msgDataDefaults = - _id: "slack-#{slackChannel.id}-#{message.ts.replace(/\./g, '-')}" - ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) - - if message.type is 'message' - if message.subtype? - if message.subtype is 'channel_join' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createUserJoinWithRoomIdAndUser room._id, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_leave' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser room._id, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'me_message' - msgObj = - msg: "_#{@convertSlackMessageToRocketChat(message.text)}_" - _.extend msgObj, msgDataDefaults - RocketChat.sendMessage @getRocketUser(message.user), msgObj, room, true - else if message.subtype is 'bot_message' or message.subtype is 'slackbot_response' - botUser = RocketChat.models.Users.findOneById 'rocket.cat', { fields: { username: 1 }} - botUsername = if @bots[message.bot_id] then @bots[message.bot_id]?.name else message.username - msgObj = - msg: @convertSlackMessageToRocketChat(message.text) - rid: room._id - bot: true - attachments: message.attachments - username: if botUsername then botUsername else undefined - - _.extend msgObj, msgDataDefaults - - if message.edited? - msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000) - editedBy = @getRocketUser(message.edited.user) - if editedBy? - msgObj.editedBy = - _id: editedBy._id - username: editedBy.username - - if message.icons? - msgObj.emoji = message.icons.emoji - - RocketChat.sendMessage botUser, msgObj, room, true - else if message.subtype is 'channel_purpose' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_description', room._id, message.purpose, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_topic' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser 'room_changed_topic', room._id, message.topic, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'channel_name' - if @getRocketUser(message.user)? - RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser room._id, message.name, @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'pinned_item' - if message.attachments - msgObj = - attachments: [ - "text" : @convertSlackMessageToRocketChat message.attachments[0].text - "author_name" : message.attachments[0].author_subname - "author_icon" : getAvatarUrlFromUsername(message.attachments[0].author_subname) - ] - _.extend msgObj, msgDataDefaults - RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgObj - else - #TODO: make this better - @logger.debug('Pinned item with no attachment, needs work.'); - #RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults - else if message.subtype is 'file_share' - if message.file?.url_private_download isnt undefined - details = - message_id: "slack-#{message.ts.replace(/\./g, '-')}" - name: message.file.name - size: message.file.size - type: message.file.mimetype - rid: room._id - @uploadFile details, message.file.url_private_download, @getRocketUser(message.user), room, new Date(parseInt(message.ts.split('.')[0]) * 1000) - else - if not missedTypes[message.subtype] and not ignoreTypes[message.subtype] - missedTypes[message.subtype] = message - else - user = @getRocketUser(message.user) - if user? - msgObj = - msg: @convertSlackMessageToRocketChat message.text - rid: room._id - u: - _id: user._id - username: user.username - - _.extend msgObj, msgDataDefaults - - if message.edited? - msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000) - editedBy = @getRocketUser(message.edited.user) - if editedBy? - msgObj.editedBy = - _id: editedBy._id - username: editedBy.username - - RocketChat.sendMessage @getRocketUser(message.user), msgObj, room, true - - # Process the reactions - if RocketChat.models.Messages.findOneById(msgDataDefaults._id)? and message.reactions?.length > 0 - for reaction in message.reactions - for u in reaction.users - rcUser = @getRocketUser(u) - if rcUser? - Meteor.runAsUser rcUser._id, () => - Meteor.call 'setReaction', ":#{reaction.name}:", msgDataDefaults._id - - @addCountCompleted 1 - - if not _.isEmpty missedTypes - console.log 'Missed import types:', missedTypes - - @updateProgress Importer.ProgressStep.FINISHING - for channel in @channels.channels when channel.do_import and channel.is_archived - do (channel) => - Meteor.runAsUser startedByUserId, () => - Meteor.call 'archiveRoom', channel.rocketId - - @updateProgress Importer.ProgressStep.DONE - timeTook = Date.now() - start - @logger.log "Import took #{timeTook} milliseconds." - - return @getProgress() - - getSlackChannelFromName: (channelName) => - for channel in @channels.channels when channel.name is channelName - return channel - - getRocketUser: (slackId) => - for user in @users.users when user.id is slackId - return RocketChat.models.Users.findOneById user.rocketId, { fields: { username: 1, name: 1 }} - - convertSlackMessageToRocketChat: (message) => - if message? - message = message.replace //g, '@all' - message = message.replace //g, '@all' - message = message.replace //g, '@here' - message = message.replace />/g, '>' - message = message.replace /</g, '<' - message = message.replace /&/g, '&' - message = message.replace /:simple_smile:/g, ':smile:' - message = message.replace /:memo:/g, ':pencil:' - message = message.replace /:piggy:/g, ':pig:' - message = message.replace /:uk:/g, ':gb:' - message = message.replace /<(http[s]?:[^>]*)>/g, '$1' - for userReplace in @userTags - message = message.replace userReplace.slack, userReplace.rocket - message = message.replace userReplace.slackLong, userReplace.rocket - else - message = '' - return message - - getSelection: () => - selectionUsers = @users.users.map (user) -> - return new Importer.SelectionUser user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot - selectionChannels = @channels.channels.map (channel) -> - return new Importer.SelectionChannel channel.id, channel.name, channel.is_archived, true, false - - return new Importer.Selection @name, selectionUsers, selectionChannels diff --git a/packages/rocketchat-importer-slack/server.js b/packages/rocketchat-importer-slack/server.js new file mode 100644 index 000000000000..ad52655f58a0 --- /dev/null +++ b/packages/rocketchat-importer-slack/server.js @@ -0,0 +1,434 @@ +/* globals Importer */ +Importer.Slack = class extends Importer.Base { + constructor(name, descriptionI18N, mimeType) { + super(name, descriptionI18N, mimeType); + this.userTags = []; + this.bots = {}; + this.logger.debug('Constructed a new Slack Importer.'); + } + prepare(dataURI, sentContentType, fileName) { + super.prepare(dataURI, sentContentType, fileName); + const {image/*, contentType*/} = RocketChatFile.dataURIParse(dataURI); + const zip = new this.AdmZip(new Buffer(image, 'base64')); + const zipEntries = zip.getEntries(); + let tempChannels = []; + let tempUsers = []; + const tempMessages = {}; + zipEntries.forEach(entry => { + if (entry.entryName.indexOf('__MACOSX') > -1) { + return this.logger.debug(`Ignoring the file: ${ entry.entryName }`); + } + if (entry.entryName === 'channels.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_CHANNELS); + tempChannels = JSON.parse(entry.getData().toString()).filter(channel => channel.creator != null); + return; + } + if (entry.entryName === 'users.json') { + this.updateProgress(Importer.ProgressStep.PREPARING_USERS); + tempUsers = JSON.parse(entry.getData().toString()); + return tempUsers.forEach(user => { + if (user.is_bot) { + this.bots[user.profile.bot_id] = user; + } + }); + } + if (!entry.isDirectory && entry.entryName.indexOf('/') > -1) { + const item = entry.entryName.split('/'); + const channelName = item[0]; + const msgGroupData = item[1].split('.')[0]; + tempMessages[channelName] = tempMessages[channelName] || {}; + try { + tempMessages[channelName][msgGroupData] = JSON.parse(entry.getData().toString()); + } catch (error) { + this.logger.warn(`${ entry.entryName } is not a valid JSON file! Unable to import it.`); + } + } + }); + + // Insert the users record, eventually this might have to be split into several ones as well + // if someone tries to import a several thousands users instance + 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); + + // Insert the channels records. + const channelsId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'channels', 'channels': tempChannels }); + this.channels = this.collection.findOne(channelsId); + this.updateRecord({ 'count.channels': tempChannels.length }); + this.addCountToTotal(tempChannels.length); + + // Insert the messages records + this.updateProgress(Importer.ProgressStep.PREPARING_MESSAGES); + + let messagesCount = 0; + Object.keys(tempMessages).forEach(channel => { + const messagesObj = tempMessages[channel]; + this.messages[channel] = this.messages[channel] || {}; + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + messagesCount += msgs.length; + this.updateRecord({ 'messagesstatus': '#{channel}/#{date}' }); + if (Importer.Base.getBSONSize(msgs) > Importer.Base.MaxBSONSize) { + const tmp = Importer.Base.getBSONSafeArraysFromAnArray(msgs); + Object.keys(tmp).forEach(i => { + const splitMsg = tmp[i]; + const messagesId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'messages', 'name': `${ channel }/${ date }.${ i }`, 'messages': splitMsg }); + this.messages[channel][`${ date }.${ i }`] = this.collection.findOne(messagesId); + }); + } else { + const messagesId = this.collection.insert({ 'import': this.importRecord._id, 'importer': this.name, 'type': 'messages', 'name': `${ channel }/${ date }`, 'messages': msgs }); + this.messages[channel][date] = this.collection.findOne(messagesId); + } + }); + }); + this.updateRecord({ 'count.messages': messagesCount, 'messagesstatus': null }); + this.addCountToTotal(messagesCount); + if ([tempUsers.length, tempChannels.length, messagesCount].some(e => e === 0)) { + this.logger.warn(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + console.log(`The loaded users count ${ tempUsers.length }, the loaded channels ${ tempChannels.length }, and the loaded messages ${ messagesCount }`); + this.updateProgress(Importer.ProgressStep.ERROR); + return this.getProgress(); + } + const selectionUsers = tempUsers.map(user => new Importer.SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); + const selectionChannels = tempChannels.map(channel => new Importer.SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + this.updateProgress(Importer.ProgressStep.USER_SELECTION); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } + startImport(importSelection) { + super.startImport(importSelection); + const start = Date.now(); + Object.keys(importSelection.users).forEach(key => { + const user = importSelection.users[key]; + Object.keys(this.users.users).forEach(k => { + const u = this.users.users[k]; + if (u.id === user.user_id) { + u.do_import = user.do_import; + } + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + Object.keys(importSelection.channels).forEach(key => { + const channel = importSelection.channels[key]; + Object.keys(this.channels.channels).forEach(k => { + const c = this.channels.channels[k]; + 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(() => { + this.updateProgress(Importer.ProgressStep.IMPORTING_USERS); + this.users.users.forEach(user => { + if (!user.do_import) { + return; + } + Meteor.runAsUser(startedByUserId, () => { + const existantUser = RocketChat.models.Users.findOneByEmailAddress(user.profile.email) || RocketChat.models.Users.findOneByUsername(user.name); + if (existantUser) { + user.rocketId = existantUser._id; + RocketChat.models.Users.update({ _id: user.rocketId }, { $addToSet: { importIds: user.id } }); + this.userTags.push({ + slack: `<@${ user.id }>`, + slackLong: `<@${ user.id }|${ user.name }>`, + rocket: `@${ existantUser.username }` + }); + } else { + const userId = user.profile.email ? Accounts.createUser({ email: user.profile.email, password: Date.now() + user.name + user.profile.email.toUpperCase() }) : Accounts.createUser({ username: user.name, password: Date.now() + user.name, joinDefaultChannelsSilenced: true }); + Meteor.runAsUser(userId, () => { + Meteor.call('setUsername', user.name, {joinDefaultChannelsSilenced: true}); + const url = user.profile.image_original || user.profile.image_512; + try { + Meteor.call('setAvatarFromService', url, undefined, 'url'); + } catch (error) { + this.logger.warn(`Failed to set ${ user.name }'s avatar from url ${ url }`); + console.log(`Failed to set ${ user.name }'s avatar from url ${ url }`); + } + // Slack's is -18000 which translates to Rocket.Chat's after dividing by 3600 + if (user.tz_offset) { + Meteor.call('userSetUtcOffset', user.tz_offset / 3600); + } + }); + + RocketChat.models.Users.update({ _id: userId }, { $addToSet: { importIds: user.id } }); + + if (user.profile.real_name) { + RocketChat.models.Users.setName(userId, user.profile.real_name); + } + //Deleted users are 'inactive' users in Rocket.Chat + if (user.deleted) { + Meteor.call('setUserActiveStatus', userId, false); + } + //TODO: Maybe send emails? + user.rocketId = userId; + this.userTags.push({ + slack: `<@${ user.id }>`, + slackLong: `<@${ user.id }|${ user.name }>`, + rocket: `@${ user.name }` + }); + + } + this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.users._id }, { $set: { 'users': this.users.users }}); + this.updateProgress(Importer.ProgressStep.IMPORTING_CHANNELS); + this.channels.channels.forEach(channel => { + if (!channel.do_import) { + return; + } + Meteor.runAsUser (startedByUserId, () => { + const existantRoom = RocketChat.models.Rooms.findOneByName(channel.name); + if (existantRoom || channel.is_general) { + if (channel.is_general && existantRoom && channel.name !== existantRoom.name) { + Meteor.call('saveRoomSettings', 'GENERAL', 'roomName', channel.name); + } + channel.rocketId = channel.is_general ? 'GENERAL' : existantRoom._id; + RocketChat.models.Rooms.update({ _id: channel.rocketId }, { $addToSet: { importIds: channel.id } }); + } else { + const users = channel.members + .reduce((ret, member) => { + if (member !== channel.creator) { + const user = this.getRocketUser(member); + if (user && user.username) { + ret.push(user.username); + } + } + return ret; + }, []); + let userId = startedByUserId; + this.users.users.forEach(user => { + if (user.id === channel.creator && user.do_import) { + userId = user.rocketId; + } + }); + Meteor.runAsUser(userId, () => { + const returned = Meteor.call('createChannel', channel.name, users); + channel.rocketId = returned.rid; + }); + + // @TODO implement model specific function + const roomUpdate = { + ts: new Date(channel.created * 1000) + }; + if (!_.isEmpty(channel.topic && channel.topic.value)) { + roomUpdate.topic = channel.topic.value; + } + if (!_.isEmpty(channel.purpose && channel.purpose.value)) { + roomUpdate.description = channel.purpose.value; + } + RocketChat.models.Rooms.update({ _id: channel.rocketId }, { $set: roomUpdate, $addToSet: { importIds: channel.id } }); + } + this.addCountCompleted(1); + }); + }); + this.collection.update({ _id: this.channels._id }, { $set: { 'channels': this.channels.channels }}); + const missedTypes = {}; + const ignoreTypes = { 'bot_add': true, 'file_comment': true, 'file_mention': true }; + this.updateProgress(Importer.ProgressStep.IMPORTING_MESSAGES); + Object.keys(this.messages).forEach(channel => { + const messagesObj = this.messages[channel]; + + Meteor.runAsUser(startedByUserId, () =>{ + const slackChannel = this.getSlackChannelFromName(channel); + if (!slackChannel || !slackChannel.do_import) { return; } + const room = RocketChat.models.Rooms.findOneById(slackChannel.rocketId, { fields: { usernames: 1, t: 1, name: 1 } }); + Object.keys(messagesObj).forEach(date => { + const msgs = messagesObj[date]; + msgs.messages.forEach(message => { + this.updateRecord({ 'messagesstatus': '#{channel}/#{date}.#{msgs.messages.length}' }); + const msgDataDefaults ={ + _id: `slack-${ slackChannel.id }-${ message.ts.replace(/\./g, '-') }`, + ts: new Date(parseInt(message.ts.split('.')[0]) * 1000) + }; + if (message.type === 'message') { + if (message.subtype) { + if (message.subtype === 'channel_join') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createUserJoinWithRoomIdAndUser(room._id, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_leave') { + if (this.getRocketUser(message.user)) { RocketChat.models.Messages.createUserLeaveWithRoomIdAndUser(room._id, this.getRocketUser(message.user), msgDataDefaults); } + } else if (message.subtype === 'me_message') { + const msgObj = { + ...msgDataDefaults, + msg: `_${ this.convertSlackMessageToRocketChat(message.text) }_` + }; + RocketChat.sendMessage(this.getRocketUser(message.user), msgObj, room, true); + } else if (message.subtype === 'bot_message' || message.subtype === 'slackbot_response') { + const botUser = RocketChat.models.Users.findOneById('rocket.cat', { fields: { username: 1 }}); + const botUsername = this.bots[message.bot_id] ? this.bots[message.bot_id].name : message.username; + const msgObj = { + ...msgDataDefaults, + msg: this.convertSlackMessageToRocketChat(message.text), + rid: room._id, + bot: true, + attachments: message.attachments, + username: botUsername || undefined + }; + + if (message.edited) { + msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); + const editedBy = this.getRocketUser(message.edited.user); + if (editedBy) { + msgObj.editedBy = { + _id: editedBy._id, + username: editedBy.username + }; + } + } + + if (message.icons) { + msgObj.emoji = message.icons.emoji; + } + RocketChat.sendMessage(botUser, msgObj, room, true); + } else if (message.subtype === 'channel_purpose') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_description', room._id, message.purpose, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_topic') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomSettingsChangedWithTypeRoomIdMessageAndUser('room_changed_topic', room._id, message.topic, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'channel_name') { + if (this.getRocketUser(message.user)) { + RocketChat.models.Messages.createRoomRenamedWithRoomIdRoomNameAndUser(room._id, message.name, this.getRocketUser(message.user), msgDataDefaults); + } + } else if (message.subtype === 'pinned_item') { + if (message.attachments) { + const msgObj = { + ...msgDataDefaults, + attachments: [{ + 'text': this.convertSlackMessageToRocketChat(message.attachments[0].text), + 'author_name' : message.attachments[0].author_subname, + 'author_icon' : getAvatarUrlFromUsername(message.attachments[0].author_subname) + }] + }; + RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser('message_pinned', room._id, '', this.getRocketUser(message.user), msgObj); + } else { + //TODO: make this better + this.logger.debug('Pinned item with no attachment, needs work.'); + //RocketChat.models.Messages.createWithTypeRoomIdMessageAndUser 'message_pinned', room._id, '', @getRocketUser(message.user), msgDataDefaults + } + } else if (message.subtype === 'file_share') { + if (message.file && message.file.url_private_download !== undefined) { + const details = { + message_id: `slack-${ message.ts.replace(/\./g, '-') }`, + name: message.file.name, + size: message.file.size, + type: message.file.mimetype, + rid: room._id + }; + this.uploadFile(details, message.file.url_private_download, this.getRocketUser(message.user), room, new Date(parseInt(message.ts.split('.')[0]) * 1000)); + } + } else if (!missedTypes[message.subtype] && !ignoreTypes[message.subtype]) { + missedTypes[message.subtype] = message; + } + } else { + const user = this.getRocketUser(message.user); + if (user) { + const msgObj = { + ...msgDataDefaults, + msg: this.convertSlackMessageToRocketChat(message.text), + rid: room._id, + u: { + _id: user._id, + username: user.username + } + }; + + if (message.edited) { + msgObj.editedAt = new Date(parseInt(message.edited.ts.split('.')[0]) * 1000); + const editedBy = this.getRocketUser(message.edited.user); + if (editedBy) { + msgObj.editedBy = { + _id: editedBy._id, + username: editedBy.username + }; + } + } + + RocketChat.sendMessage(this.getRocketUser(message.user), msgObj, room, true); + } + } + } + + + // Process the reactions + if (RocketChat.models.Messages.findOneById(msgDataDefaults._id) && message.reactions && message.reactions.length > 0) { + message.reactions.forEach(reaction => { + reaction.users.forEach(u => { + const rcUser = this.getRocketUser(u); + if (!rcUser) { return; } + Meteor.runAsUser(rcUser._id, () => Meteor.call('setReaction', `:${ reaction.name }:`, msgDataDefaults._id)); + }); + }); + } + this.addCountCompleted(1); + }); + }); + }); + }); + + + if (!_.isEmpty(missedTypes)) { + console.log('Missed import types:', missedTypes); + } + + this.updateProgress(Importer.ProgressStep.FINISHING); + + this.channels.channels.forEach(channel => { + if (channel.do_import && channel.is_archived) { + Meteor.runAsUser(startedByUserId, function() { + Meteor.call('archiveRoom', channel.rocketId); + }); + } + }); + this.updateProgress(Importer.ProgressStep.DONE); + + const timeTook = Date.now() - start; + + this.logger.log(`Import took ${ timeTook } milliseconds.`); + + }); + return this.getProgress(); + } + getSlackChannelFromName(channelName) { + return this.channels.channels.find(channel => channel.name === channelName); + } + getRocketUser(slackId) { + const user = this.users.users.find(user => user.id === slackId); + if (user) { + return RocketChat.models.Users.findOneById(user.rocketId, { fields: { username: 1, name: 1 }}); + } + } + convertSlackMessageToRocketChat(message) { + if (message != null) { + message = message.replace(//g, '@all'); + message = message.replace(//g, '@all'); + message = message.replace(//g, '@here'); + message = message.replace(/>/g, '>'); + message = message.replace(/</g, '<'); + message = message.replace(/&/g, '&'); + message = message.replace(/:simple_smile:/g, ':smile:'); + message = message.replace(/:memo:/g, ':pencil:'); + message = message.replace(/:piggy:/g, ':pig:'); + message = message.replace(/:uk:/g, ':gb:'); + message = message.replace(/<(http[s]?:[^>]*)>/g, '$1'); + for (const userReplace of Array.from(this.userTags)) { + message = message.replace(userReplace.slack, userReplace.rocket); + message = message.replace(userReplace.slackLong, userReplace.rocket); + } + } else { + message = ''; + } + return message; + } + getSelection() { + const selectionUsers = this.users.users.map(user => new Importer.SelectionUser(user.id, user.name, user.profile.email, user.deleted, user.is_bot, !user.is_bot)); + const selectionChannels = this.channels.channels.map(channel => new Importer.SelectionChannel(channel.id, channel.name, channel.is_archived, true, false)); + return new Importer.Selection(this.name, selectionUsers, selectionChannels); + } +};