- {{#unless isAdmin}}
+ {{#unless hasPermission 'run-import'}}
{{_ "You_are_not_authorized_to_view_this_page"}}
{{else}}
+ {{#if isPreparing}}
+ {{> loading}}
+ {{else}}
-
-
{{_ "Import History" }}
+
+
+ {{#if anySuccessfulSlackImports}}
+
+ {{/if}}
+
-
-
{{_ "View the status of all import operations." }}
-
-
+ {{#if canShowCurrentOperation}}
+
+
{{_ "Current_Import_Operation"}}
+
-
+
+ {{> importOperationSummary operation}}
+
+ {{#if canContinueOperation}}
+
+ {{else}}
+ {{#if canCheckOperationProgress}}
+
+ {{/if}}
+ {{/if}}
+
+ {{/if}}
-
+ {{#if history}}
+
+
{{_ "Recent_Import_History"}}
+
-
diff --git a/app/importer/client/admin/adminImport.js b/app/importer/client/admin/adminImport.js
index 09956ade17bbd..ddbf8fc0f8d63 100644
--- a/app/importer/client/admin/adminImport.js
+++ b/app/importer/client/admin/adminImport.js
@@ -1,45 +1,131 @@
-import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import { ReactiveVar } from 'meteor/reactive-var';
+import toastr from 'toastr';
-import { hasRole } from '../../../authorization';
-import { t, handleError } from '../../../utils';
+import { t, APIClient } from '../../../utils';
import { SideNav } from '../../../ui-utils/client';
+import { ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ProgressStep } from '../../lib/ImporterProgressStep';
-import { Importers } from '..';
+import './adminImport.html';
+import './importOperationSummary.js';
Template.adminImport.helpers({
- isAdmin() {
- return hasRole(Meteor.userId(), 'admin');
+ isPreparing() {
+ return Template.instance().preparing.get();
},
- getDescription(importer) {
- return TAPi18n.__('Importer_From_Description', { from: t(importer.name) });
+ history() {
+ return Template.instance().history.get();
},
- importers() {
- return Importers.getAll();
+
+ operation() {
+ return Template.instance().operation.get();
+ },
+
+ isNotCurrentOperation() {
+ const operation = Template.instance().operation.get();
+ if (!operation) {
+ return true;
+ }
+
+ return operation._id !== this._id || !operation.valid;
+ },
+
+ canShowCurrentOperation() {
+ const operation = Template.instance().operation.get();
+ return operation && operation.valid;
+ },
+
+ canContinueOperation() {
+ const operation = Template.instance().operation.get();
+ if (!operation || !operation.valid) {
+ return false;
+ }
+
+ const possibleStatus = [ProgressStep.USER_SELECTION].concat(ImportWaitingStates).concat(ImportFileReadyStates).concat(ImportPreparingStartedStates);
+ return possibleStatus.includes(operation.status);
+ },
+
+ canCheckOperationProgress() {
+ const operation = Template.instance().operation.get();
+ if (!operation || !operation.valid) {
+ return false;
+ }
+
+ return ImportingStartedStates.includes(operation.status);
+ },
+
+ anySuccessfulSlackImports() {
+ const history = Template.instance().history.get();
+ if (!history) {
+ return false;
+ }
+
+ for (const op of history) {
+ if (op.importerKey === 'slack' && op.status === ProgressStep.DONE) {
+ return true;
+ }
+ }
+
+ return false;
},
});
Template.adminImport.events({
- 'click .import-history'() {
- FlowRouter.go('/admin/import/history');
+ 'click .new-import-btn'() {
+ FlowRouter.go('/admin/import/new');
+ },
+ 'click .download-slack-files-btn'(event, template) {
+ template.preparing.set(true);
+ APIClient.post('v1/downloadPendingFiles').then((data) => {
+ template.preparing.set(false);
+ if (data.count) {
+ toastr.success(t('File_Downloads_Started'));
+ FlowRouter.go('/admin/import/progress');
+ } else {
+ toastr.success(t('No_files_left_to_download'));
+ }
+ }).catch((error) => {
+ template.preparing.set(false);
+ if (error) {
+ console.error(error);
+ toastr.error(t('Failed_To_Download_Files'));
+ }
+ });
},
+ 'click .prepare-btn'() {
+ FlowRouter.go('/admin/import/prepare');
+ },
+ 'click .progress-btn'() {
+ FlowRouter.go('/admin/import/progress');
+ },
+});
+
+Template.adminImport.onCreated(function() {
+ const instance = this;
+ this.preparing = new ReactiveVar(true);
+ this.history = new ReactiveVar([]);
+ this.operation = new ReactiveVar(false);
- 'click .start-import'() {
- const importer = this;
+ APIClient.get('v1/getCurrentImportOperation').then((data) => {
+ instance.operation.set(data.operation);
- Meteor.call('setupImporter', importer.key, function(error) {
+ APIClient.get('v1/getLatestImportOperations').then((data) => {
+ instance.history.set(data);
+ instance.preparing.set(false);
+ }).catch((error) => {
if (error) {
- console.log(t('importer_setup_error'), importer.key, error);
- handleError(error);
- return;
+ toastr.error(t('Failed_To_Load_Import_History'));
+ instance.preparing.set(false);
}
-
- FlowRouter.go(`/admin/import/prepare/${ importer.key }`);
});
- },
+ }).catch((error) => {
+ if (error) {
+ toastr.error(t('Failed_To_Load_Import_Operation'));
+ instance.preparing.set(false);
+ }
+ });
});
Template.adminImport.onRendered(() => {
diff --git a/app/importer/client/admin/adminImportHistory.html b/app/importer/client/admin/adminImportHistory.html
deleted file mode 100644
index 4dc635dc11754..0000000000000
--- a/app/importer/client/admin/adminImportHistory.html
+++ /dev/null
@@ -1,135 +0,0 @@
-
-
- {{> header sectionName="Recent_Import_History" ullpage="true"}}
-
- {{#unless isAdmin}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{> loading}}
- {{else}}
-
-
- {{#each history}}
-
-
-
-
{{ statusMessage}}
-
- {{_ "Import_Type"}}:
- {{ type}} [{{ importerKey}}]
-
-
- {{_ "Last_Updated"}}:
- {{ lastUpdated}}
-
-
- {{#if messagesstatus}}
- {{_ "Current_File"}}:
- {{ messagesstatus}}
-
- {{/if}}
-
- {{#if hasCounters}}
- {{_ "Counters"}}:
-
-
- {{_ "Users"}}:
- {{ userCount}}
-
- {{_ "Channels"}}:
- {{ channelCount}}
-
- {{_ "Messages"}}:
- {{ messageCount}}
-
- {{_ "Total"}}:
- {{ totalCount}}
-
-
-
- {{/if}}
-
- {{#if hasErrors}}
- {{_ "Errors_and_Warnings"}}:
-
-
- {{#if fileData.users}}
- {{#each fileData.users}}
- {{#if is_email_taken}}
- {{_ "Email_already_exists"}}:
- {{ email}}
-
- {{/if}}
-
- {{#if error}}
- {{ email}}:
- {{ formatedError}}
-
- {{/if}}
-
- {{/each}}
- {{/if}}
-
- {{#if errors}}
-
-
- {{_ "Not_Imported_Messages_Title"}}
-
- {{#each errors}}
-
- {{#if msg}}
- {{#if msg.id}}
- {{_ "Message_Id"}}:
- {{ msg.id}}
-
- {{/if}}
- {{#if msg.userId}}
- {{_ "Message_UserId"}}:
- {{ msg.userId}}
-
- {{/if}}
- {{#if msg.ts}}
- {{_ "Message_Time"}}:
- {{ messageTime}}
-
- {{/if}}
-
- {{/if}}
- {{#if error}}
- {{#if error.error}}
- {{_ "Error"}}:
- {{ error.error}}:
-
- {{/if}}
- {{#if error.message}}
- {{_ "Message"}}:
- {{ error.message}}:
-
- {{/if}}
- {{#if error.details}}
- {{#if error.details.method}}
- {{_ "Method"}}:
- {{ error.details.method}}:
-
- {{/if}}
- {{/if}}
- {{/if}}
- {{/each}}
- {{/if}}
-
-
- {{/if}}
-
-
-
- {{/each}}
- {{/if}}
- {{/unless}}
-
-
-
diff --git a/app/importer/client/admin/adminImportHistory.js b/app/importer/client/admin/adminImportHistory.js
deleted file mode 100644
index 2fb1ccf928c42..0000000000000
--- a/app/importer/client/admin/adminImportHistory.js
+++ /dev/null
@@ -1,205 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { Tracker } from 'meteor/tracker';
-import { Template } from 'meteor/templating';
-import toastr from 'toastr';
-import { ReactiveVar } from 'meteor/reactive-var';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-
-import { hasRole } from '../../../authorization';
-import { t, APIClient } from '../../../utils';
-import { ProgressStep } from '../../lib/ImporterProgressStep';
-import { SideNav } from '../../../ui-utils/client';
-
-import { Importers } from '..';
-
-Template.adminImportHistory.helpers({
- isAdmin() {
- return hasRole(Meteor.userId(), 'admin');
- },
- importers() {
- return Importers.getAll();
- },
- isPreparing() {
- return Template.instance().preparing.get();
- },
- history() {
- return Template.instance().history.get();
- },
- statusMessage() {
- const statusKey = this.status || ProgressStep.NEW;
-
- switch (statusKey) {
- case ProgressStep.NEW:
- return t('Not_started');
- case ProgressStep.UPLOADING:
- return t('Uploading_file');
- case ProgressStep.DOWNLOADING_FILE_URL:
- return t('Downloading_file_from_external_URL');
- case ProgressStep.DOWNLOAD_COMPLETE:
- return t('Successfully_downloaded_file_from_external_URL_should_start_preparing_soon');
- case ProgressStep.PREPARING_STARTED:
- return t('Preparing_data_for_import_process');
- case ProgressStep.PREPARING_USERS:
- return t('Preparing_list_of_users');
- case ProgressStep.PREPARING_CHANNELS:
- return t('Preparing_list_of_channels');
- case ProgressStep.PREPARING_MESSAGES:
- return t('Preparing_list_of_messages');
- case ProgressStep.USER_SELECTION:
- return t('Selecting_users');
- case ProgressStep.IMPORTING_STARTED:
- return t('Started');
- case ProgressStep.IMPORTING_USERS:
- return t('Importing_users');
- case ProgressStep.IMPORTING_CHANNELS:
- return t('Importing_channels');
- case ProgressStep.IMPORTING_MESSAGES:
- return t('Importing_messages');
- case ProgressStep.FINISHING:
- return t('Almost_done');
- case ProgressStep.DONE:
- return t('Completed');
- case ProgressStep.ERROR:
- return t('Error');
- case ProgressStep.CANCELLED:
- return t('Canceled');
- default:
- return statusKey;
- }
- },
-
- lastUpdated() {
- if (!this._updatedAt) {
- return '';
- }
-
- const date = new Date(this._updatedAt);
- return date.toLocaleString();
- },
-
- hasCounters() {
- return Boolean(this.count);
- },
-
- userCount() {
- if (this.count && this.count.users) {
- return this.count.users;
- }
-
- return 0;
- },
-
- channelCount() {
- if (this.count && this.count.channels) {
- return this.count.channels;
- }
-
- return 0;
- },
-
- messageCount() {
- if (this.count && this.count.messages) {
- return this.count.messages;
- }
-
- return 0;
- },
-
- totalCount() {
- if (this.count && this.count.total) {
- return this.count.total;
- }
-
- return 0;
- },
-
- hasErrors() {
- if (!this.fileData) {
- return false;
- }
-
- if (this.fileData.users) {
- for (const user of this.fileData.users) {
- if (user.is_email_taken) {
- return true;
- }
- if (user.error) {
- return true;
- }
- }
- }
-
- if (this.errors && this.errors.length > 0) {
- return true;
- }
-
- return false;
- },
-
- formatedError() {
- if (!this.error) {
- return '';
- }
-
- if (typeof this.error === 'string') {
- return this.error;
- }
-
- if (typeof this.error === 'object') {
- if (this.error.message) {
- return this.error.message;
- }
- if (this.error.error && typeof this.error.error === 'string') {
- return this.error.error;
- }
-
- try {
- const json = JSON.stringify(this.error);
- console.log(json);
- return json;
- } catch (e) {
- return t('Error');
- }
- }
-
- return this.error.toString();
- },
-
- messageTime() {
- if (!this.msg || !this.msg.ts) {
- return '';
- }
-
- const date = new Date(this.msg.ts);
- return date.toLocaleString();
- },
-});
-
-Template.adminImportHistory.events({
- 'click .import-list'() {
- FlowRouter.go('/admin/import');
- },
-});
-
-Template.adminImportHistory.onCreated(function() {
- const instance = this;
- this.preparing = new ReactiveVar(true);
- this.history = new ReactiveVar([]);
-
- APIClient.get('v1/getLatestImportOperations').then((data) => {
- instance.history.set(data);
- instance.preparing.set(false);
- }).catch((error) => {
- if (error) {
- toastr.error(t('Failed_To_Load_Import_Data'));
- instance.preparing.set(false);
- }
- });
-});
-
-Template.adminImportHistory.onRendered(() => {
- Tracker.afterFlush(() => {
- SideNav.setFlex('adminFlex');
- SideNav.openFlex();
- });
-});
diff --git a/app/importer/client/admin/adminImportNew.html b/app/importer/client/admin/adminImportNew.html
new file mode 100644
index 0000000000000..17e9230b5cb24
--- /dev/null
+++ b/app/importer/client/admin/adminImportNew.html
@@ -0,0 +1,83 @@
+
+
+ {{> header sectionName=pageTitle}}
+
+
{{_ "Back_to_imports"}}
+
+ {{#unless hasPermission 'run-import'}}
+
{{_ "You_are_not_authorized_to_view_this_page"}}
+ {{else}}
+ {{#if isPreparing}}
+ {{> loading}}
+ {{else}}
+
+
+
+
+ {{/if}}
+
+ {{/unless}}
+
+
+
diff --git a/app/importer/client/admin/adminImportNew.js b/app/importer/client/admin/adminImportNew.js
new file mode 100644
index 0000000000000..2d643312dc1c2
--- /dev/null
+++ b/app/importer/client/admin/adminImportNew.js
@@ -0,0 +1,158 @@
+import { Tracker } from 'meteor/tracker';
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { Template } from 'meteor/templating';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import { ReactiveVar } from 'meteor/reactive-var';
+import toastr from 'toastr';
+
+import { t, APIClient } from '../../../utils';
+import { SideNav } from '../../../ui-utils/client';
+import { settings } from '../../../settings';
+import { showImporterException } from '../functions/showImporterException';
+
+import { Importers } from '..';
+
+import './adminImportNew.html';
+
+Template.adminImportNew.helpers({
+ isPreparing() {
+ return Template.instance().preparing.get();
+ },
+ importers() {
+ return Importers.getAll();
+ },
+ pageTitle() {
+ const importerKey = Template.instance().importType.get();
+ if (!importerKey) {
+ return t('Import_New_File');
+ }
+
+ const importer = Importers.get(importerKey);
+ if (!importer) {
+ return t('Import_New_File');
+ }
+
+ return TAPi18n.__('Importer_From_Description', { from: t(importer.name) });
+ },
+ importType() {
+ return Template.instance().importType.get();
+ },
+ fileType() {
+ return Template.instance().fileType.get();
+ },
+ isImporterSelected() {
+ return Template.instance().importType.get();
+ },
+ isFileTypeSelected() {
+ return Template.instance().fileType.get();
+ },
+ isUpload() {
+ return Template.instance().fileType.get() === 'upload';
+ },
+ isPublicURL() {
+ return Template.instance().fileType.get() === 'url';
+ },
+ isServerFile() {
+ return Template.instance().fileType.get() === 'path';
+ },
+ fileSizeLimitMessage() {
+ const maxFileSize = settings.get('FileUpload_MaxFileSize');
+ let message;
+
+ if (maxFileSize > 0) {
+ const sizeInKb = maxFileSize / 1024;
+ const sizeInMb = sizeInKb / 1024;
+
+ let fileSizeMessage;
+ if (sizeInMb > 0) {
+ fileSizeMessage = TAPi18n.__('FileSize_MB', { fileSize: sizeInMb.toFixed(2) });
+ } else if (sizeInKb > 0) {
+ fileSizeMessage = TAPi18n.__('FileSize_KB', { fileSize: sizeInKb.toFixed(2) });
+ } else {
+ fileSizeMessage = TAPi18n.__('FileSize_Bytes', { fileSize: maxFileSize.toFixed(0) });
+ }
+
+ message = TAPi18n.__('Importer_Upload_FileSize_Message', { maxFileSize: fileSizeMessage });
+ } else {
+ message = TAPi18n.__('Importer_Upload_Unlimited_FileSize');
+ }
+
+ return message;
+ },
+});
+
+Template.adminImportNew.events({
+ 'change .file-type'(event, template) {
+ template.fileType.set($('select[name=file-type]').val());
+ },
+ 'change .import-type'(event, template) {
+ template.importType.set($('select[name=import-type]').val());
+ },
+
+ 'change .import-file-input'(event, template) {
+ const importType = template.importType.get();
+
+ const e = event.originalEvent || event;
+ let { files } = e.target;
+ if (!files || (files.length === 0)) {
+ files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || [];
+ }
+
+ Array.from(files).forEach((file) => {
+ template.preparing.set(true);
+
+ const reader = new FileReader();
+
+ reader.readAsDataURL(file);
+ reader.onloadend = () => {
+ APIClient.post('v1/uploadImportFile', {
+ binaryContent: reader.result.split(';base64,')[1],
+ contentType: file.type,
+ fileName: file.name,
+ importerKey: importType,
+ }).then(() => {
+ toastr.success(t('File_uploaded_successfully'));
+ FlowRouter.go('/admin/import/prepare');
+ }).catch((error) => {
+ if (error) {
+ showImporterException(error);
+ template.preparing.set(false);
+ }
+ });
+ };
+ });
+ },
+
+ 'click .import-btn'(event, template) {
+ const importType = template.importType.get();
+ const fileUrl = $('.import-file-url').val();
+
+ template.preparing.set(true);
+
+ APIClient.post('v1/downloadPublicImportFile', {
+ fileUrl,
+ importerKey: importType,
+ }).then(() => {
+ toastr.success(t('Import_requested_successfully'));
+ FlowRouter.go('/admin/import/prepare');
+ }).catch((error) => {
+ if (error) {
+ showImporterException(error);
+ template.preparing.set(false);
+ }
+ });
+ },
+});
+
+Template.adminImportNew.onCreated(function() {
+ this.preparing = new ReactiveVar(false);
+ this.importType = new ReactiveVar('');
+ this.fileType = new ReactiveVar('upload');
+});
+
+Template.adminImportNew.onRendered(() => {
+ Tracker.afterFlush(() => {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+});
diff --git a/app/importer/client/admin/adminImportPrepare.html b/app/importer/client/admin/adminImportPrepare.html
index 2d817c635382c..54f4fc2369f4d 100644
--- a/app/importer/client/admin/adminImportPrepare.html
+++ b/app/importer/client/admin/adminImportPrepare.html
@@ -1,100 +1,71 @@
- {{#with importer}}
- {{> header sectionName=name fullpage="true"}}
-
- {{#unless isAdmin}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
+ {{> header sectionName=pageTitle}}
+
+ {{#unless hasPermission 'run-import'}}
+
{{_ "You_are_not_authorized_to_view_this_page"}}
+ {{else}}
+ {{#if isPreparing}}
+ {{#if hasProgressRate}}
+ {{ progressRate }}
+ {{/if}}
+
+ {{> loading}}
{{else}}
-
- {{/unless}}
-
- {{/with}}
+ {{#if channels.length}}
+
+ {{/if}}
+ {{/if}}
+ {{/unless}}
+
diff --git a/app/importer/client/admin/adminImportPrepare.js b/app/importer/client/admin/adminImportPrepare.js
index 479a791f9b275..f37dc66a4b803 100644
--- a/app/importer/client/admin/adminImportPrepare.js
+++ b/app/importer/client/admin/adminImportPrepare.js
@@ -1,32 +1,35 @@
-import { Meteor } from 'meteor/meteor';
-import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import { ReactiveVar } from 'meteor/reactive-var';
import toastr from 'toastr';
-import { hasRole } from '../../../authorization';
-import { settings } from '../../../settings';
-import { t, handleError, APIClient } from '../../../utils';
+import { t, APIClient } from '../../../utils';
import { SideNav } from '../../../ui-utils/client';
+import { ProgressStep, ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ImportingErrorStates } from '../../lib/ImporterProgressStep';
+import { showImporterException } from '../functions/showImporterException';
-import { Importers } from '..';
+import { ImporterWebsocketReceiver } from '..';
+
+import './adminImportPrepare.html';
Template.adminImportPrepare.helpers({
- isAdmin() {
- return hasRole(Meteor.userId(), 'admin');
+ isPreparing() {
+ return Template.instance().preparing.get();
+ },
+ hasProgressRate() {
+ return Template.instance().progressRate.get() !== false;
},
- importer() {
- const importerKey = FlowRouter.getParam('importer');
+ progressRate() {
+ const rate = Template.instance().progressRate.get();
+ if (rate) {
+ return `${ rate }%`;
+ }
- return Importers.get(importerKey);
+ return '';
},
- isLoaded() {
- return Template.instance().loaded.get();
- },
- isPreparing() {
- return Template.instance().preparing.get();
+ pageTitle() {
+ return t('Importing_Data');
},
users() {
return Template.instance().users.get();
@@ -37,282 +40,154 @@ Template.adminImportPrepare.helpers({
message_count() {
return Template.instance().message_count.get();
},
- fileSizeLimitMessage() {
- const maxFileSize = settings.get('FileUpload_MaxFileSize');
- let message;
-
- if (maxFileSize > 0) {
- const sizeInKb = maxFileSize / 1024;
- const sizeInMb = sizeInKb / 1024;
-
- let fileSizeMessage;
- if (sizeInMb > 0) {
- fileSizeMessage = TAPi18n.__('FileSize_MB', { fileSize: sizeInMb.toFixed(2) });
- } else if (sizeInKb > 0) {
- fileSizeMessage = TAPi18n.__('FileSize_KB', { fileSize: sizeInKb.toFixed(2) });
- } else {
- fileSizeMessage = TAPi18n.__('FileSize_Bytes', { fileSize: maxFileSize.toFixed(0) });
- }
-
- message = TAPi18n.__('Importer_Upload_FileSize_Message', { maxFileSize: fileSizeMessage });
- } else {
- message = TAPi18n.__('Importer_Upload_Unlimited_FileSize');
- }
-
- return message;
- },
});
-function showToastrForErrorType(errorType, defaultErrorString) {
- let errorCode;
-
- if (typeof errorType === 'string') {
- errorCode = errorType;
- } else if (typeof errorType === 'object') {
- if (errorType.error && typeof errorType.error === 'string') {
- errorCode = errorType.error;
+Template.adminImportPrepare.events({
+ 'click .button.start'(event, template) {
+ const btn = this;
+ $(btn).prop('disabled', true);
+ for (const user of Array.from(template.users.get())) {
+ user.do_import = $(`[name='${ user.user_id }']`).is(':checked');
}
- }
- if (errorCode) {
- const errorTranslation = t(errorCode);
- if (errorTranslation !== errorCode) {
- toastr.error(errorTranslation);
- return;
+ for (const channel of Array.from(template.channels.get())) {
+ channel.do_import = $(`[name='${ channel.channel_id }']`).is(':checked');
}
- }
- toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File'));
-}
+ APIClient.post('v1/startImport', { input: { users: template.users.get(), channels: template.channels.get() } }).then(() => {
+ template.users.set([]);
+ template.channels.set([]);
+ return FlowRouter.go('/admin/import/progress');
+ }).catch((error) => {
+ if (error) {
+ showImporterException(error, 'Failed_To_Start_Import');
+ return FlowRouter.go('/admin/import');
+ }
+ });
+ },
-function showException(error, defaultErrorString) {
- console.error(error);
- if (error && error.xhr && error.xhr.responseJSON) {
- console.log(error.xhr.responseJSON);
+ 'click .button.uncheck-deleted-users'(event, template) {
+ Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => {
+ const box = $(`[name=${ user.user_id }]`);
+ return box && box.length && box[0].checked && box.click();
+ });
+ },
- if (error.xhr.responseJSON.errorType) {
- showToastrForErrorType(error.xhr.responseJSON.errorType, defaultErrorString);
- return;
- }
- }
+ 'click .button.uncheck-archived-channels'(event, template) {
+ Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => {
+ const box = $(`[name=${ channel.channel_id }]`);
+ return box && box.length && box[0].checked && box.click();
+ });
+ },
- toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File'));
-}
+});
-function getImportFileData(importer, template) {
- APIClient.get(`v1/getImportFileData?importerKey=${ importer.key }`).then((data) => {
+function getImportFileData(template) {
+ APIClient.get('v1/getImportFileData').then((data) => {
if (!data) {
- console.warn(`The importer ${ importer.key } is not set up correctly, as it did not return any data.`);
+ console.warn('The importer is not set up correctly, as it did not return any data.');
toastr.error(t('Importer_not_setup'));
- template.preparing.set(false);
- return;
+ return FlowRouter.go('/admin/import');
}
if (data.waiting) {
setTimeout(() => {
- getImportFileData(importer, template);
+ getImportFileData(template);
}, 1000);
return;
}
if (data.step) {
console.warn('Invalid file, contains `data.step`.', data);
- toastr.error(t('Invalid_Export_File', importer.key));
- template.preparing.set(false);
- return;
+ toastr.error(t('Failed_To_Load_Import_Data'));
+ return FlowRouter.go('/admin/import');
}
template.users.set(data.users);
template.channels.set(data.channels);
template.message_count.set(data.message_count);
- template.loaded.set(true);
template.preparing.set(false);
+ template.progressRate.set(false);
}).catch((error) => {
if (error) {
- showException(error, 'Failed_To_Load_Import_Data');
- template.preparing.set(false);
+ showImporterException(error, 'Failed_To_Load_Import_Data');
+ return FlowRouter.go('/admin/import');
}
});
}
-Template.adminImportPrepare.events({
- 'change .import-file-input'(event, template) {
- const importer = this;
- if (!importer || !importer.key) { return; }
+function loadOperation(template) {
+ APIClient.get('v1/getCurrentImportOperation').then((data) => {
+ const { operation } = data;
- const e = event.originalEvent || event;
- let { files } = e.target;
- if (!files || (files.length === 0)) {
- files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || [];
+ if (!operation.valid) {
+ return FlowRouter.go('/admin/import/new');
}
- Array.from(files).forEach((file) => {
- template.preparing.set(true);
-
- const reader = new FileReader();
-
- reader.readAsBinaryString(file);
- reader.onloadend = () => {
- APIClient.post('v1/uploadImportFile', {
- binaryContent: reader.result,
- contentType: file.type,
- fileName: file.name,
- importerKey: importer.key,
- }).then(() => {
- getImportFileData(importer, template);
- }).catch((error) => {
- if (error) {
- showException(error);
- template.preparing.set(false);
- }
- });
- };
- });
- },
-
-
- 'click .download-public-url'(event, template) {
- const importer = this;
- if (!importer || !importer.key) { return; }
-
- const fileUrl = $('.import-file-url').val();
+ // If the import has already started, move to the progress screen
+ if (ImportingStartedStates.includes(operation.status)) {
+ return FlowRouter.go('/admin/import/progress');
+ }
- template.preparing.set(true);
+ // The getImportFileData method can handle it if the state is:
+ // 1) ready to select the users,
+ // 2) preparing
+ // 3) ready to be prepared
- APIClient.post('v1/downloadPublicImportFile', {
- fileUrl,
- importerKey: importer.key,
- }).then(() => {
- getImportFileData(importer, template);
- }).catch((error) => {
- if (error) {
- showException(error);
- template.preparing.set(false);
+ if (operation.status === ProgressStep.USER_SELECTION || ImportPreparingStartedStates.includes(operation.status) || ImportFileReadyStates.includes(operation.status)) {
+ if (!template.callbackRegistered) {
+ ImporterWebsocketReceiver.registerCallback(template.progressUpdated);
+ template.callbackRegistered = true;
}
- });
- },
-
- 'click .button.start'(event, template) {
- const btn = this;
- $(btn).prop('disabled', true);
- for (const user of Array.from(template.users.get())) {
- user.do_import = $(`[name='${ user.user_id }']`).is(':checked');
+ getImportFileData(template);
+ return template.preparing.set(true);
}
- for (const channel of Array.from(template.channels.get())) {
- channel.do_import = $(`[name='${ channel.channel_id }']`).is(':checked');
- }
-
- Meteor.call('startImport', FlowRouter.getParam('importer'), { users: template.users.get(), channels: template.channels.get() }, function(error) {
- if (error) {
- console.warn('Error on starting the import:', error);
- handleError(error);
- } else {
- FlowRouter.go(`/admin/import/progress/${ FlowRouter.getParam('importer') }`);
- }
- });
- },
+ // We're still waiting for a file... This shouldn't take long
+ if (ImportWaitingStates.includes(operation.status)) {
+ setTimeout(() => {
+ loadOperation(template);
+ }, 1000);
- 'click .button.restart'(event, template) {
- Meteor.call('restartImport', FlowRouter.getParam('importer'), function(error) {
- if (error) {
- console.warn('Error while restarting the import:', error);
- handleError(error);
- return;
- }
+ return template.preparing.set(true);
+ }
- template.users.set([]);
- template.channels.set([]);
- template.loaded.set(false);
- });
- },
+ if (ImportingErrorStates.includes(operation.status)) {
+ toastr.error(t('Import_Operation_Failed'));
+ return FlowRouter.go('/admin/import');
+ }
- 'click .button.uncheck-deleted-users'(event, template) {
- Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => {
- const box = $(`[name=${ user.user_id }]`);
- return box && box.length && box[0].checked && box.click();
- });
- },
+ if (operation.status === ProgressStep.DONE) {
+ return FlowRouter.go('/admin/import');
+ }
- 'click .button.uncheck-archived-channels'(event, template) {
- Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => {
- const box = $(`[name=${ channel.channel_id }]`);
- return box && box.length && box[0].checked && box.click();
- });
- },
-});
+ toastr.error(t('Unknown_Import_State'));
+ return FlowRouter.go('/admin/import');
+ }).catch((error) => {
+ if (error) {
+ toastr.error(t('Failed_To_Load_Import_Data'));
+ return FlowRouter.go('/admin/import');
+ }
+ });
+}
Template.adminImportPrepare.onCreated(function() {
- const instance = this;
this.preparing = new ReactiveVar(true);
- this.loaded = new ReactiveVar(false);
+ this.progressRate = new ReactiveVar(false);
+ this.callbackRegistered = false;
this.users = new ReactiveVar([]);
this.channels = new ReactiveVar([]);
this.message_count = new ReactiveVar(0);
- function loadSelection(progress) {
- if (progress != null ? progress.step : undefined) {
- switch (progress.step) {
- // When the import is running, take the user to the progress page
- case 'importer_importing_started':
- case 'importer_importing_users':
- case 'importer_importing_channels':
- case 'importer_importing_messages':
- case 'importer_finishing':
- return FlowRouter.go(`/admin/import/progress/${ FlowRouter.getParam('importer') }`);
- case 'importer_user_selection':
- return Meteor.call('getSelectionData', FlowRouter.getParam('importer'), function(error, data) {
- if (error) {
- handleError(error);
- }
- instance.users.set(data.users);
- instance.channels.set(data.channels);
- instance.message_count.set(data.message_count);
- instance.loaded.set(true);
- return instance.preparing.set(false);
- });
- case 'importer_new':
- return instance.preparing.set(false);
- default:
- return Meteor.call('restartImport', FlowRouter.getParam('importer'), function(error, progress) {
- if (error) {
- handleError(error);
- }
- return loadSelection(progress);
- });
- }
- } else {
- return console.warn('Invalid progress information.', progress);
+ this.progressUpdated = (progress) => {
+ if ('rate' in progress) {
+ const { rate } = progress;
+ this.progressRate.set(rate);
}
- }
+ };
- // Load the initial progress to determine what we need to do
- if (FlowRouter.getParam('importer')) {
- return Meteor.call('getImportProgress', FlowRouter.getParam('importer'), function(error, progress) {
- if (error) {
- console.warn('Error while getting the import progress:', error);
- handleError(error);
- return;
- }
-
- // if the progress isnt defined, that means there currently isn't an instance
- // of the importer, so we need to create it
- if (progress === undefined) {
- return Meteor.call('setupImporter', FlowRouter.getParam('importer'), function(err, data) {
- if (err) {
- handleError(err);
- }
- instance.preparing.set(false);
- return loadSelection(data);
- });
- }
- // Otherwise, we might need to do something based upon the current step
- // of the import
- return loadSelection(progress);
- });
- }
- return FlowRouter.go('/admin/import');
+ loadOperation(this);
});
Template.adminImportPrepare.onRendered(() => {
@@ -321,3 +196,8 @@ Template.adminImportPrepare.onRendered(() => {
SideNav.openFlex();
});
});
+
+Template.adminImportPrepare.onDestroyed(function() {
+ this.callbackRegistered = false;
+ ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated);
+});
diff --git a/app/importer/client/admin/adminImportProgress.html b/app/importer/client/admin/adminImportProgress.html
index 9acfb08a8ec12..05337b38b150a 100644
--- a/app/importer/client/admin/adminImportProgress.html
+++ b/app/importer/client/admin/adminImportProgress.html
@@ -2,6 +2,7 @@
{{> loading}}
{{step}}
{{completed}} / {{total}}
+
{{progressRate}}
{{_ "You_can_close_this_window_now"}}
diff --git a/app/importer/client/admin/adminImportProgress.js b/app/importer/client/admin/adminImportProgress.js
index bb7f5f930e03b..cf59ab3c79cc9 100644
--- a/app/importer/client/admin/adminImportProgress.js
+++ b/app/importer/client/admin/adminImportProgress.js
@@ -1,12 +1,14 @@
-import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import toastr from 'toastr';
-import { t, handleError } from '../../../utils';
+import { t, handleError, APIClient } from '../../../utils';
+import { ProgressStep, ImportingStartedStates } from '../../lib/ImporterProgressStep';
-import { Importers, ImporterWebsocketReceiver, ProgressStep } from '..';
+import { ImporterWebsocketReceiver } from '..';
+
+import './adminImportProgress.html';
Template.adminImportProgress.helpers({
step() {
@@ -18,20 +20,33 @@ Template.adminImportProgress.helpers({
total() {
return Template.instance().total.get();
},
+ progressRate() {
+ try {
+ const instance = Template.instance();
+ const completed = instance.completed.get();
+ const total = instance.total.get();
+
+ const rate = Math.floor(completed * 10000 / total) / 100;
+
+ if (isNaN(rate)) {
+ return '';
+ }
+
+ return `${ rate }%`;
+ } catch {
+ return '';
+ }
+ },
});
Template.adminImportProgress.onCreated(function() {
- const instance = this;
+ const template = this;
+ this.operation = new ReactiveVar(false);
this.step = new ReactiveVar(t('Loading...'));
this.completed = new ReactiveVar(0);
this.total = new ReactiveVar(0);
- // Ensure there is an importer how they're accessing it
- const key = FlowRouter.getParam('importer').toLowerCase();
- if (key === '' || !Importers.get(key)) {
- FlowRouter.go('/admin/import');
- return;
- }
+ let importerKey = false;
function _updateProgress(progress) {
switch (progress.step) {
@@ -39,46 +54,84 @@ Template.adminImportProgress.onCreated(function() {
toastr.success(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
return FlowRouter.go('/admin/import');
case ProgressStep.ERROR:
+ case ProgressStep.CANCELLED:
toastr.error(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
- return FlowRouter.go(`/admin/import/prepare/${ key }`);
+ return FlowRouter.go('/admin/import');
default:
- instance.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
- instance.completed.set(progress.count.completed);
- instance.total.set(progress.count.total);
+ template.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1)));
+ if (progress.count.completed) {
+ template.completed.set(progress.count.completed);
+ }
+ if (progress.count.total) {
+ template.total.set(progress.count.total);
+ }
break;
}
}
this.progressUpdated = function _progressUpdated(progress) {
- if (progress.key.toLowerCase() !== key) {
+ if (progress.key.toLowerCase() !== importerKey) {
return;
}
_updateProgress(progress);
};
- Meteor.call('getImportProgress', key, function(error, progress) {
- if (error) {
- console.warn('Error on getting the import progress:', error);
- handleError(error);
+ APIClient.get('v1/getCurrentImportOperation').then((data) => {
+ const { operation } = data;
+
+ if (!operation.valid) {
return FlowRouter.go('/admin/import');
}
- if (!progress) {
- toastr.warning(t('Importer_not_in_progress'));
- return FlowRouter.go(`/admin/import/prepare/${ key }`);
+ // If the import has not started, move to the prepare screen
+ if (!ImportingStartedStates.includes(operation.status)) {
+ return FlowRouter.go('/admin/import/prepare');
+ }
+
+ importerKey = operation.importerKey;
+ template.operation.set(operation);
+ if (operation.count) {
+ if (operation.count.total) {
+ template.total.set(operation.count.total);
+ }
+ if (operation.count.completed) {
+ template.completed.set(operation.count.completed);
+ }
}
- const whereTo = _updateProgress(progress);
+ APIClient.get('v1/getImportProgress').then((progress) => {
+ if (!progress) {
+ toastr.warning(t('Importer_not_in_progress'));
+ return FlowRouter.go('/admin/import/prepare');
+ }
+
+ const whereTo = _updateProgress(progress);
+
+ if (!whereTo) {
+ ImporterWebsocketReceiver.registerCallback(template.progressUpdated);
+ }
+ }).catch((error) => {
+ console.warn('Error on getting the import progress:', error);
- if (!whereTo) {
- ImporterWebsocketReceiver.registerCallback(instance.progressUpdated);
+ if (error) {
+ handleError(error);
+ } else {
+ toastr.error(t('Failed_To_Load_Import_Data'));
+ }
+
+ return FlowRouter.go('/admin/import');
+ });
+ }).catch((error) => {
+ if (error) {
+ handleError(error);
+ } else {
+ toastr.error(t('Failed_To_Load_Import_Data'));
}
+ return FlowRouter.go('/admin/import');
});
});
Template.adminImportProgress.onDestroyed(function() {
- const instance = this;
-
- ImporterWebsocketReceiver.unregisterCallback(instance.progressUpdated);
+ ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated);
});
diff --git a/app/importer/client/admin/importOperationSummary.html b/app/importer/client/admin/importOperationSummary.html
new file mode 100644
index 0000000000000..2378a4ebddc80
--- /dev/null
+++ b/app/importer/client/admin/importOperationSummary.html
@@ -0,0 +1,115 @@
+
+ {{_ "Import_Type"}}:
+ {{ type}} [{{ importerKey}}]
+
+
+ {{_ "Last_Updated"}}:
+ {{ lastUpdated}}
+
+
+ {{#if valid}}
+ {{_ "Status"}}:
+ {{else}}
+ {{_ "Last_Status"}}:
+ {{/if}}
+
+ {{ status}}
+
+
+ {{#if file}}
+ {{_ "File"}}:
+ {{ fileName}}
+
+ {{/if}}
+
+ {{#if hasCounters}}
+ {{_ "Counters"}}:
+
+
+ {{_ "Users"}}:
+ {{ userCount}}
+
+ {{_ "Channels"}}:
+ {{ channelCount}}
+
+ {{_ "Messages"}}:
+ {{ messageCount}}
+
+ {{_ "Total"}}:
+ {{ totalCount}}
+
+
+
+ {{/if}}
+
+ {{#if hasErrors}}
+ {{_ "Errors_and_Warnings"}}:
+
+
+ {{#if fileData.users}}
+ {{#each fileData.users}}
+ {{#if is_email_taken}}
+ {{_ "Email_already_exists"}}:
+ {{ email}}
+
+ {{/if}}
+
+ {{#if error}}
+ {{ email}}:
+ {{ formatedError}}
+
+ {{/if}}
+
+ {{/each}}
+ {{/if}}
+
+ {{#if errors}}
+
+
+ {{_ "Not_Imported_Messages_Title"}}
+
+ {{#each errors}}
+
+ {{#if msg}}
+ {{#if msg.id}}
+ {{_ "Message_Id"}}:
+ {{ msg.id}}
+
+ {{/if}}
+ {{#if msg.userId}}
+ {{_ "Message_UserId"}}:
+ {{ msg.userId}}
+
+ {{/if}}
+ {{#if msg.ts}}
+ {{_ "Message_Time"}}:
+ {{ messageTime}}
+
+ {{/if}}
+ {{/if}}
+ {{#if error}}
+ {{#if error.error}}
+ {{_ "Error"}}:
+ {{ error.error}}:
+
+ {{/if}}
+ {{#if error.message}}
+ {{_ "Message"}}:
+ {{ error.message}}:
+
+ {{/if}}
+ {{#if error.details}}
+ {{#if error.details.method}}
+ {{_ "Method"}}:
+ {{ error.details.method}}:
+
+ {{/if}}
+ {{/if}}
+ {{/if}}
+ {{/each}}
+ {{/if}}
+
+
+ {{/if}}
+
+
diff --git a/app/importer/client/admin/importOperationSummary.js b/app/importer/client/admin/importOperationSummary.js
new file mode 100644
index 0000000000000..0fb5293a044ad
--- /dev/null
+++ b/app/importer/client/admin/importOperationSummary.js
@@ -0,0 +1,136 @@
+import { Template } from 'meteor/templating';
+
+import { t } from '../../../utils';
+
+import './importOperationSummary.html';
+
+Template.importOperationSummary.helpers({
+ lastUpdated() {
+ if (!this._updatedAt) {
+ return '';
+ }
+
+ const date = new Date(this._updatedAt);
+ return date.toLocaleString();
+ },
+
+ status() {
+ if (!this.status) {
+ return '';
+ }
+
+ return t(this.status.replace('importer_', 'importer_status_'));
+ },
+
+ fileName() {
+ const fileName = this.file;
+ if (!fileName) {
+ return '';
+ }
+
+ // If the userid is inside the filename, remove it and anything before it
+ const idx = fileName.indexOf(`_${ this.user }_`);
+ if (idx >= 0) {
+ return fileName.substring(idx + this.user.length + 2);
+ }
+
+ return fileName;
+ },
+
+ hasCounters() {
+ return Boolean(this.count);
+ },
+
+ userCount() {
+ if (this.count && this.count.users) {
+ return this.count.users;
+ }
+
+ return 0;
+ },
+
+ channelCount() {
+ if (this.count && this.count.channels) {
+ return this.count.channels;
+ }
+
+ return 0;
+ },
+
+ messageCount() {
+ if (this.count && this.count.messages) {
+ return this.count.messages;
+ }
+
+ return 0;
+ },
+
+ totalCount() {
+ if (this.count && this.count.total) {
+ return this.count.total;
+ }
+
+ return 0;
+ },
+
+ hasErrors() {
+ if (!this.fileData) {
+ return false;
+ }
+
+ if (this.fileData.users) {
+ for (const user of this.fileData.users) {
+ if (user.is_email_taken) {
+ return true;
+ }
+ if (user.error) {
+ return true;
+ }
+ }
+ }
+
+ if (this.errors && this.errors.length > 0) {
+ return true;
+ }
+
+ return false;
+ },
+
+ formatedError() {
+ if (!this.error) {
+ return '';
+ }
+
+ if (typeof this.error === 'string') {
+ return this.error;
+ }
+
+ if (typeof this.error === 'object') {
+ if (this.error.message) {
+ return this.error.message;
+ }
+ if (this.error.error && typeof this.error.error === 'string') {
+ return this.error.error;
+ }
+
+ try {
+ const json = JSON.stringify(this.error);
+ console.log(json);
+ return json;
+ } catch (e) {
+ return t('Error');
+ }
+ }
+
+ return this.error.toString();
+ },
+
+ messageTime() {
+ if (!this.msg || !this.msg.ts) {
+ return '';
+ }
+
+ const date = new Date(this.msg.ts);
+ return date.toLocaleString();
+ },
+});
diff --git a/app/importer/client/admin/views.js b/app/importer/client/admin/views.js
deleted file mode 100644
index 5e6c2fc3ed0e4..0000000000000
--- a/app/importer/client/admin/views.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import './adminImport.html';
-import './adminImport';
-import './adminImportHistory.html';
-import './adminImportHistory';
-import './adminImportPrepare.html';
-import './adminImportPrepare';
-import './adminImportProgress.html';
-import './adminImportProgress';
diff --git a/app/importer/client/functions/showImporterException.js b/app/importer/client/functions/showImporterException.js
new file mode 100644
index 0000000000000..4312e48148419
--- /dev/null
+++ b/app/importer/client/functions/showImporterException.js
@@ -0,0 +1,41 @@
+import toastr from 'toastr';
+
+import { t } from '../../../utils';
+
+function showToastrForErrorType(errorType, defaultErrorString) {
+ let errorCode;
+
+ if (typeof errorType === 'string') {
+ errorCode = errorType;
+ } else if (typeof errorType === 'object') {
+ if (errorType.error && typeof errorType.error === 'string') {
+ errorCode = errorType.error;
+ }
+ }
+
+ if (errorCode) {
+ const errorTranslation = t(errorCode);
+ if (errorTranslation !== errorCode) {
+ toastr.error(errorTranslation);
+ return;
+ }
+ }
+
+ toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File'));
+}
+
+function showException(error, defaultErrorString) {
+ console.error(error);
+ if (error && error.xhr && error.xhr.responseJSON) {
+ console.log(error.xhr.responseJSON);
+
+ if (error.xhr.responseJSON.errorType) {
+ showToastrForErrorType(error.xhr.responseJSON.errorType, defaultErrorString);
+ return;
+ }
+ }
+
+ toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File'));
+}
+
+export const showImporterException = showException;
diff --git a/app/importer/client/index.js b/app/importer/client/index.js
index 68c66ec4b4eab..1ae15dfc877f7 100644
--- a/app/importer/client/index.js
+++ b/app/importer/client/index.js
@@ -6,35 +6,34 @@ import { Importers } from '../lib/Importers';
import { ImporterInfo } from '../lib/ImporterInfo';
import { ProgressStep } from '../lib/ImporterProgressStep';
-
FlowRouter.route('/admin/import', {
name: 'admin-import',
async action() {
- await import('./admin/views');
+ await import('./admin/adminImport');
BlazeLayout.render('main', { center: 'adminImport' });
},
});
-FlowRouter.route('/admin/import/history', {
- name: 'admin-import-history',
+FlowRouter.route('/admin/import/new', {
+ name: 'admin-import-new',
async action() {
- await import('./admin/views');
- BlazeLayout.render('main', { center: 'adminImportHistory' });
+ await import('./admin/adminImportNew');
+ BlazeLayout.render('main', { center: 'adminImportNew' });
},
});
-FlowRouter.route('/admin/import/prepare/:importer', {
+FlowRouter.route('/admin/import/prepare', {
name: 'admin-import-prepare',
async action() {
- await import('./admin/views');
+ await import('./admin/adminImportPrepare');
BlazeLayout.render('main', { center: 'adminImportPrepare' });
},
});
-FlowRouter.route('/admin/import/progress/:importer', {
+FlowRouter.route('/admin/import/progress', {
name: 'admin-import-progress',
async action() {
- await import('./admin/views');
+ await import('./admin/adminImportProgress');
BlazeLayout.render('main', { center: 'adminImportProgress' });
},
});
diff --git a/app/importer/lib/ImporterProgressStep.js b/app/importer/lib/ImporterProgressStep.js
index 8571ccb533fa9..e61db4e77f7f0 100644
--- a/app/importer/lib/ImporterProgressStep.js
+++ b/app/importer/lib/ImporterProgressStep.js
@@ -1,20 +1,62 @@
/** The progress step that an importer is at. */
export const ProgressStep = Object.freeze({
- UPLOADING: 'importer_uploading',
NEW: 'importer_new',
- DOWNLOADING_FILE_URL: 'downloading_file_url',
- DOWNLOAD_COMPLETE: 'download_complete',
+ UPLOADING: 'importer_uploading',
+ DOWNLOADING_FILE: 'importer_downloading_file',
+ FILE_LOADED: 'importer_file_loaded',
+
PREPARING_STARTED: 'importer_preparing_started',
PREPARING_USERS: 'importer_preparing_users',
PREPARING_CHANNELS: 'importer_preparing_channels',
PREPARING_MESSAGES: 'importer_preparing_messages',
+
USER_SELECTION: 'importer_user_selection',
+
IMPORTING_STARTED: 'importer_importing_started',
IMPORTING_USERS: 'importer_importing_users',
IMPORTING_CHANNELS: 'importer_importing_channels',
IMPORTING_MESSAGES: 'importer_importing_messages',
+ IMPORTING_FILES: 'importer_importing_files',
FINISHING: 'importer_finishing',
+
DONE: 'importer_done',
ERROR: 'importer_import_failed',
CANCELLED: 'importer_import_cancelled',
});
+
+export const ImportWaitingStates = [
+ ProgressStep.NEW,
+ ProgressStep.UPLOADING,
+ ProgressStep.DOWNLOADING_FILE,
+];
+
+export const ImportFileReadyStates = [
+ ProgressStep.FILE_LOADED,
+];
+
+export const ImportPreparingStartedStates = [
+ ProgressStep.PREPARING_STARTED,
+ ProgressStep.PREPARING_USERS,
+ ProgressStep.PREPARING_CHANNELS,
+ ProgressStep.PREPARING_MESSAGES,
+];
+
+export const ImportingStartedStates = [
+ ProgressStep.IMPORTING_STARTED,
+ ProgressStep.IMPORTING_USERS,
+ ProgressStep.IMPORTING_CHANNELS,
+ ProgressStep.IMPORTING_MESSAGES,
+ ProgressStep.IMPORTING_FILES,
+ ProgressStep.FINISHING,
+];
+
+export const ImportingDoneStates = [
+ ProgressStep.DONE,
+ ProgressStep.ERROR,
+ ProgressStep.CANCELLED,
+];
+
+export const ImportingErrorStates = [
+ ProgressStep.ERROR,
+ ProgressStep.CANCELLED,
+];
diff --git a/app/importer/server/classes/ImporterBase.js b/app/importer/server/classes/ImporterBase.js
index f7cebb0f885c4..d672c81d38de9 100644
--- a/app/importer/server/classes/ImporterBase.js
+++ b/app/importer/server/classes/ImporterBase.js
@@ -10,10 +10,9 @@ import { Progress } from './ImporterProgress';
import { Selection } from './ImporterSelection';
import { ImporterWebsocket } from './ImporterWebsocket';
import { ProgressStep } from '../../lib/ImporterProgressStep';
-import { Imports } from '../models/Imports';
import { ImporterInfo } from '../../lib/ImporterInfo';
import { RawImports } from '../models/RawImports';
-import { Settings } from '../../../models';
+import { Settings, Imports } from '../../../models';
import { Logger } from '../../../logger';
import { FileUpload } from '../../../file-upload';
import { sendMessage } from '../../../lib';
@@ -76,7 +75,7 @@ export class Base {
* @param {string} description The i18n string which describes the importer
* @param {string} mimeType The expected file type.
*/
- constructor(info) {
+ constructor(info, importRecord) {
if (!(info instanceof ImporterInfo)) {
throw new Error('Information passed in must be a valid ImporterInfo instance.');
}
@@ -88,7 +87,6 @@ export class Base {
this.prepare = this.prepare.bind(this);
this.startImport = this.startImport.bind(this);
- this.getSelection = this.getSelection.bind(this);
this.getProgress = this.getProgress.bind(this);
this.updateProgress = this.updateProgress.bind(this);
this.addCountToTotal = this.addCountToTotal.bind(this);
@@ -103,7 +101,6 @@ export class Base {
this.collection = RawImports;
const userId = Meteor.userId();
- const importRecord = Imports.findPendingImport(this.info.key);
if (importRecord) {
this.logger.debug('Found existing import operation');
@@ -202,15 +199,6 @@ export class Base {
return this.updateProgress(ProgressStep.IMPORTING_STARTED);
}
- /**
- * Gets the Selection object for the import.
- *
- * @returns {Selection} The users and channels selection
- */
- getSelection() {
- throw new Error(`Invalid 'getSelection' called on ${ this.info.name }, it must be overridden and super can not be called.`);
- }
-
/**
* Gets the progress of this import.
*
@@ -262,11 +250,21 @@ export class Base {
this.logger.debug(`${ this.info.name } is now at ${ step }.`);
this.updateRecord({ status: this.progress.step });
- ImporterWebsocket.progressUpdated(this.progress);
+ this.reportProgress();
return this.progress;
}
+ reloadCount() {
+ if (!this.importRecord.count) {
+ this.progress.count.total = 0;
+ this.progress.count.completed = 0;
+ }
+
+ this.progress.count.total = this.importRecord.count.total || 0;
+ this.progress.count.completed = this.importRecord.count.completed || 0;
+ }
+
/**
* Adds the passed in value to the total amount of items needed to complete.
*
@@ -295,12 +293,19 @@ export class Base {
this.updateRecord({ 'count.completed': this.progress.count.completed });
}
- ImporterWebsocket.progressUpdated(this.progress);
+ this.reportProgress();
this.logger.log(`${ this.progress.count.completed } messages imported`);
return this.progress;
}
+ /**
+ * Sends an updated progress to the websocket
+ */
+ reportProgress() {
+ ImporterWebsocket.progressUpdated(this.progress);
+ }
+
/**
* Registers error information on a specific user from the import record
*
@@ -388,7 +393,7 @@ export class Base {
if (err) {
throw new Error(err);
} else {
- const url = file.url.replace(Meteor.absoluteUrl(), '/');
+ const url = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`);
const attachment = {
title: file.name,
diff --git a/app/importer/server/index.js b/app/importer/server/index.js
index 489bcabf5c87b..0fed91e5b6608 100644
--- a/app/importer/server/index.js
+++ b/app/importer/server/index.js
@@ -1,5 +1,4 @@
import { Base } from './classes/ImporterBase';
-import { Imports } from './models/Imports';
import { ImporterWebsocket } from './classes/ImporterWebsocket';
import { Progress } from './classes/ImporterProgress';
import { RawImports } from './models/RawImports';
@@ -10,10 +9,6 @@ import { ProgressStep } from '../lib/ImporterProgressStep';
import { ImporterInfo } from '../lib/ImporterInfo';
import { Importers } from '../lib/Importers';
import './methods/getImportProgress';
-import './methods/getSelectionData';
-import './methods/prepareImport';
-import './methods/restartImport';
-import './methods/setupImporter';
import './methods/startImport';
import './methods/uploadImportFile';
import './methods/getImportFileData';
@@ -24,7 +19,6 @@ import './startup/store';
export {
Base,
- Imports,
Importers,
ImporterInfo,
ImporterWebsocket,
diff --git a/app/importer/server/methods/downloadPublicImportFile.js b/app/importer/server/methods/downloadPublicImportFile.js
index 914a324b99506..fa8a20a6f73cc 100644
--- a/app/importer/server/methods/downloadPublicImportFile.js
+++ b/app/importer/server/methods/downloadPublicImportFile.js
@@ -1,17 +1,18 @@
import http from 'http';
+import https from 'https';
import fs from 'fs';
import { Meteor } from 'meteor/meteor';
-
import { RocketChatImportFileInstance } from '../startup/store';
import { ProgressStep } from '../../lib/ImporterProgressStep';
-import { hasRole } from '../../../authorization';
+import { hasPermission } from '../../../authorization';
import { Importers } from '..';
function downloadHttpFile(fileUrl, writeStream) {
- http.get(fileUrl, function(response) {
+ const protocol = fileUrl.startsWith('https') ? https : http;
+ protocol.get(fileUrl, function(response) {
response.pipe(writeStream);
});
}
@@ -29,8 +30,8 @@ Meteor.methods({
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'downloadPublicImportFile' });
}
- if (!hasRole(userId, 'admin')) {
- throw new Meteor.Error('not_authorized', 'User not authorized', { method: 'downloadPublicImportFile' });
+ if (!hasPermission(userId, 'run-import')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'downloadPublicImportFile' });
}
const importer = Importers.get(importerKey);
@@ -38,39 +39,47 @@ Meteor.methods({
throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'downloadPublicImportFile' });
}
+ const isUrl = fileUrl.startsWith('http');
+
+ // Check if it's a valid url or path before creating a new import record
+ if (!isUrl) {
+ if (!fs.existsSync(fileUrl)) {
+ throw new Meteor.Error('error-import-file-missing', fileUrl, { method: 'downloadPublicImportFile' });
+ }
+ }
+
+ importer.instance = new importer.importer(importer); // eslint-disable-line new-cap
+
const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1);
const date = new Date();
const dateStr = `${ date.getUTCFullYear() }${ date.getUTCMonth() }${ date.getUTCDate() }${ date.getUTCHours() }${ date.getUTCMinutes() }${ date.getUTCSeconds() }`;
const newFileName = `${ dateStr }_${ userId }_${ oldFileName }`;
+ // Store the file name on the imports collection
importer.instance.startFileUpload(newFileName);
- importer.instance.updateProgress(ProgressStep.DOWNLOADING_FILE_URL);
+ importer.instance.updateProgress(ProgressStep.DOWNLOADING_FILE);
const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName);
- if (fileUrl.startsWith('http')) {
+ writeStream.on('error', Meteor.bindEnvironment(() => {
+ importer.instance.updateProgress(ProgressStep.ERROR);
+ }));
+
+ writeStream.on('end', Meteor.bindEnvironment(() => {
+ importer.instance.updateProgress(ProgressStep.FILE_LOADED);
+ }));
+
+ if (isUrl) {
downloadHttpFile(fileUrl, writeStream);
} else {
- if (!fs.existsSync(fileUrl)) {
- throw new Meteor.Error('error-import-file-missing', fileUrl, { method: 'downloadPublicImportFile' });
- }
-
// If the url is actually a folder path on the current machine, skip moving it to the file store
if (fs.statSync(fileUrl).isDirectory()) {
importer.instance.updateRecord({ file: fileUrl });
- importer.instance.updateProgress(ProgressStep.DOWNLOAD_COMPLETE);
+ importer.instance.updateProgress(ProgressStep.FILE_LOADED);
return;
}
copyLocalFile(fileUrl, writeStream);
}
-
- writeStream.on('error', Meteor.bindEnvironment(() => {
- importer.instance.updateProgress(ProgressStep.ERROR);
- }));
-
- writeStream.on('end', Meteor.bindEnvironment(() => {
- importer.instance.updateProgress(ProgressStep.DOWNLOAD_COMPLETE);
- }));
},
});
diff --git a/app/importer/server/methods/getImportFileData.js b/app/importer/server/methods/getImportFileData.js
index 9265313d9cd54..58b5a2439b9fe 100644
--- a/app/importer/server/methods/getImportFileData.js
+++ b/app/importer/server/methods/getImportFileData.js
@@ -4,34 +4,40 @@ import fs from 'fs';
import { Meteor } from 'meteor/meteor';
import { RocketChatImportFileInstance } from '../startup/store';
-import { hasRole } from '../../../authorization';
+import { hasPermission } from '../../../authorization';
+import { Imports } from '../../../models';
import { ProgressStep } from '../../lib/ImporterProgressStep';
import { Importers } from '..';
Meteor.methods({
- getImportFileData(importerKey) {
+ getImportFileData() {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getImportFileData' });
}
- if (!hasRole(userId, 'admin')) {
- throw new Meteor.Error('not_authorized', 'User not authorized', { method: 'getImportFileData' });
+ if (!hasPermission(userId, 'run-import')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'getImportFileData' });
}
+ const operation = Imports.findLastImport();
+ if (!operation) {
+ throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', { method: 'getImportFileData' });
+ }
+
+ const { importerKey } = operation;
+
const importer = Importers.get(importerKey);
if (!importer) {
throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'getImportFileData' });
}
- if (!importer.instance) {
- return undefined;
- }
+ importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
const waitingSteps = [
- ProgressStep.DOWNLOADING_FILE_URL,
+ ProgressStep.DOWNLOADING_FILE,
ProgressStep.PREPARING_CHANNELS,
ProgressStep.PREPARING_MESSAGES,
ProgressStep.PREPARING_USERS,
diff --git a/app/importer/server/methods/getImportProgress.js b/app/importer/server/methods/getImportProgress.js
index a7f5f33d4bed4..220e2e2519f7d 100644
--- a/app/importer/server/methods/getImportProgress.js
+++ b/app/importer/server/methods/getImportProgress.js
@@ -1,12 +1,13 @@
import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
+import { Imports } from '../../../models';
import { Importers } from '..';
Meteor.methods({
- getImportProgress(key) {
+ getImportProgress() {
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getImportProgress' });
}
@@ -15,15 +16,18 @@ Meteor.methods({
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' });
}
- const importer = Importers.get(key);
+ const operation = Imports.findLastImport();
+ if (!operation) {
+ throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', { method: 'getImportProgress' });
+ }
+ const { importerKey } = operation;
+ const importer = Importers.get(importerKey);
if (!importer) {
- throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'getImportProgress' });
+ throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'getImportProgress' });
}
- if (!importer.instance) {
- return undefined;
- }
+ importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
return importer.instance.getProgress();
},
diff --git a/app/importer/server/methods/getLatestImportOperations.js b/app/importer/server/methods/getLatestImportOperations.js
index af710a67ecc1b..14980ace21b60 100644
--- a/app/importer/server/methods/getLatestImportOperations.js
+++ b/app/importer/server/methods/getLatestImportOperations.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
-import { Imports } from '../models/Imports';
+import { Imports } from '../../../models';
import { hasRole } from '../../../authorization';
Meteor.methods({
diff --git a/app/importer/server/methods/getSelectionData.js b/app/importer/server/methods/getSelectionData.js
deleted file mode 100644
index b49573f1487c4..0000000000000
--- a/app/importer/server/methods/getSelectionData.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasPermission } from '../../../authorization';
-
-import {
- Importers,
- ProgressStep,
-} from '..';
-
-
-Meteor.methods({
- getSelectionData(key) {
- if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSelectionData' });
- }
-
- if (!hasPermission(Meteor.userId(), 'run-import')) {
- throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' });
- }
-
- const importer = Importers.get(key);
-
- if (!importer || !importer.instance) {
- throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'getSelectionData' });
- }
-
- const progress = importer.instance.getProgress();
-
- switch (progress.step) {
- case ProgressStep.USER_SELECTION:
- return importer.instance.getSelection();
- default:
- return undefined;
- }
- },
-});
diff --git a/app/importer/server/methods/prepareImport.js b/app/importer/server/methods/prepareImport.js
deleted file mode 100644
index 9f7b763c61cad..0000000000000
--- a/app/importer/server/methods/prepareImport.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { check } from 'meteor/check';
-
-import { hasPermission } from '../../../authorization';
-
-import { Importers } from '..';
-
-
-Meteor.methods({
- prepareImport(key, dataURI, contentType, fileName) {
- if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'prepareImport' });
- }
-
- if (!hasPermission(Meteor.userId(), 'run-import')) {
- throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' });
- }
-
- check(key, String);
- check(dataURI, String);
- check(fileName, String);
-
- const importer = Importers.get(key);
-
- if (!importer) {
- throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'prepareImport' });
- }
-
- const results = importer.instance.prepare(dataURI, contentType, fileName);
-
- if (results instanceof Promise) {
- return results.catch((e) => { throw new Meteor.Error(e); });
- }
- return results;
- },
-});
diff --git a/app/importer/server/methods/restartImport.js b/app/importer/server/methods/restartImport.js
deleted file mode 100644
index f8fb60dcd48e9..0000000000000
--- a/app/importer/server/methods/restartImport.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasPermission } from '../../../authorization';
-
-import {
- Importers,
- ProgressStep,
-} from '..';
-
-
-Meteor.methods({
- restartImport(key) {
- if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'restartImport' });
- }
-
- if (!hasPermission(Meteor.userId(), 'run-import')) {
- throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' });
- }
-
- const importer = Importers.get(key);
-
- if (!importer) {
- throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'restartImport' });
- }
-
- if (importer.instance) {
- if (importer.instance.importRecord) {
- importer.instance.updateProgress(ProgressStep.CANCELLED);
- importer.instance.updateRecord({ valid: false });
- }
- importer.instance = undefined;
- }
-
- importer.instance = new importer.importer(importer); // eslint-disable-line new-cap
- return importer.instance.getProgress();
- },
-});
diff --git a/app/importer/server/methods/setupImporter.js b/app/importer/server/methods/setupImporter.js
deleted file mode 100644
index a5f8c3cfa1f19..0000000000000
--- a/app/importer/server/methods/setupImporter.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasPermission } from '../../../authorization';
-
-import { Importers } from '..';
-
-
-Meteor.methods({
- setupImporter(key) {
- if (!Meteor.userId()) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setupImporter' });
- }
-
- if (!hasPermission(Meteor.userId(), 'run-import')) {
- throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'setupImporter' });
- }
-
- const importer = Importers.get(key);
-
- if (!importer) {
- console.warn(`Tried to setup ${ name } as an importer.`);
- throw new Meteor.Error('error-importer-not-defined', 'The importer was not defined correctly, it is missing the Import class.', { method: 'setupImporter' });
- }
-
- if (importer.instance) {
- return importer.instance.getProgress();
- }
-
- importer.instance = new importer.importer(importer); // eslint-disable-line new-cap
- return importer.instance.getProgress();
- },
-});
diff --git a/app/importer/server/methods/startImport.js b/app/importer/server/methods/startImport.js
index 540b06bec0848..c6651b4d38879 100644
--- a/app/importer/server/methods/startImport.js
+++ b/app/importer/server/methods/startImport.js
@@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { hasPermission } from '../../../authorization';
+import { Imports } from '../../../models';
import {
Importers,
@@ -11,7 +12,7 @@ import {
Meteor.methods({
- startImport(key, input) {
+ startImport(input) {
// Takes name and object with users / channels selected to import
if (!Meteor.userId()) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'startImport' });
@@ -21,16 +22,19 @@ Meteor.methods({
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'startImport' });
}
- if (!key) {
- throw new Meteor.Error('error-invalid-importer', `No defined importer by: "${ key }"`, { method: 'startImport' });
+ const operation = Imports.findLastImport();
+ if (!operation) {
+ throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', { method: 'startImport' });
}
- const importer = Importers.get(key);
-
- if (!importer || !importer.instance) {
- throw new Meteor.Error('error-importer-not-defined', `The importer (${ key }) has no import class defined.`, { method: 'startImport' });
+ const { importerKey } = operation;
+ const importer = Importers.get(importerKey);
+ if (!importer) {
+ throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'startImport' });
}
+ importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
+
const usersSelection = input.users.map((user) => new SelectionUser(user.user_id, user.username, user.email, user.is_deleted, user.is_bot, user.do_import));
const channelsSelection = input.channels.map((channel) => new SelectionChannel(channel.channel_id, channel.name, channel.is_archived, channel.do_import));
diff --git a/app/importer/server/methods/uploadImportFile.js b/app/importer/server/methods/uploadImportFile.js
index d83776530ca49..00baa31c8bbf0 100644
--- a/app/importer/server/methods/uploadImportFile.js
+++ b/app/importer/server/methods/uploadImportFile.js
@@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor';
import { RocketChatFile } from '../../../file';
import { RocketChatImportFileInstance } from '../startup/store';
-import { hasRole } from '../../../authorization';
+import { hasPermission } from '../../../authorization';
+import { ProgressStep } from '../../lib/ImporterProgressStep';
import { Importers } from '..';
@@ -15,8 +16,8 @@ Meteor.methods({
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'uploadImportFile' });
}
- if (!hasRole(userId, 'admin')) {
- throw new Meteor.Error('not_authorized', 'User not authorized', { method: 'uploadImportFile' });
+ if (!hasPermission(userId, 'run-import')) {
+ throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', { method: 'uploadImportFile' });
}
const importer = Importers.get(importerKey);
@@ -24,16 +25,24 @@ Meteor.methods({
throw new Meteor.Error('error-importer-not-defined', `The importer (${ importerKey }) has no import class defined.`, { method: 'uploadImportFile' });
}
+ importer.instance = new importer.importer(importer); // eslint-disable-line new-cap
+
const date = new Date();
const dateStr = `${ date.getUTCFullYear() }${ date.getUTCMonth() }${ date.getUTCDate() }${ date.getUTCHours() }${ date.getUTCMinutes() }${ date.getUTCSeconds() }`;
const newFileName = `${ dateStr }_${ userId }_${ fileName }`;
+ // Store the file name and content type on the imports collection
importer.instance.startFileUpload(newFileName, contentType);
- const file = new Buffer(binaryContent, 'binary');
+ // Save the file on the File Store
+ const file = new Buffer(binaryContent, 'base64');
const readStream = RocketChatFile.bufferToStream(file);
const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType);
+ writeStream.on('end', Meteor.bindEnvironment(() => {
+ importer.instance.updateProgress(ProgressStep.FILE_LOADED);
+ }));
+
readStream.pipe(writeStream);
},
});
diff --git a/app/importer/server/models/Imports.js b/app/importer/server/models/Imports.js
deleted file mode 100644
index 1bba254a81cad..0000000000000
--- a/app/importer/server/models/Imports.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { ProgressStep } from '../../lib/ImporterProgressStep';
-import { Base } from '../../../models';
-
-class ImportsModel extends Base {
- constructor() {
- super('import');
- }
-
- findPendingImport(key) {
- // Finds the latest import operation
- const data = this.findOne({ importerKey: key }, { createdAt: -1 });
- if (!data || !data.status) {
- return data;
- }
-
- // But only returns it if it is still pending
-
- const forbiddenStatus = [
- ProgressStep.DONE,
- ProgressStep.ERROR,
- ProgressStep.CANCELLED,
- ];
-
- if (forbiddenStatus.indexOf(data.status) >= 0) {
- return undefined;
- }
-
- return data;
- }
-}
-
-export const Imports = new ImportsModel();
diff --git a/app/importer/server/startup/setImportsToInvalid.js b/app/importer/server/startup/setImportsToInvalid.js
index 2622f2eb6c062..c5303516cb597 100644
--- a/app/importer/server/startup/setImportsToInvalid.js
+++ b/app/importer/server/startup/setImportsToInvalid.js
@@ -1,18 +1,40 @@
import { Meteor } from 'meteor/meteor';
-import { Imports } from '../models/Imports';
+import { Imports } from '../../../models';
import { RawImports } from '../models/RawImports';
+import { ProgressStep } from '../../lib/ImporterProgressStep';
-Meteor.startup(function() {
- // Make sure all imports are marked as invalid, data clean up since you can't
- // restart an import at the moment.
- Imports.update({ valid: { $ne: false } }, { $set: { valid: false } }, { multi: true });
-
- // Clean up all the raw import data, since you can't restart an import at the moment
+function runDrop(fn) {
try {
- RawImports.model.rawCollection().drop();
+ fn();
} catch (e) {
console.log('errror', e); // TODO: Remove
// ignored
}
+}
+
+Meteor.startup(function() {
+ const lastOperation = Imports.findLastImport();
+ let idToKeep = false;
+
+ // If the operation is ready to start, or already started but failed
+ // 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;
+ }
+ }
+
+ if (idToKeep) {
+ Imports.invalidateOperationsExceptId(idToKeep);
+
+ // Clean up all the raw import data, except for the last operation
+ runDrop(() => RawImports.model.rawCollection().remove({ import: { $ne: idToKeep } }));
+ } else {
+ Imports.invalidateAllOperations();
+
+ // Clean up all the raw import data
+ runDrop(() => RawImports.model.rawCollection().drop());
+ }
});
diff --git a/app/integrations/client/route.js b/app/integrations/client/route.js
index 7ee526094c701..18cd9743bfe65 100644
--- a/app/integrations/client/route.js
+++ b/app/integrations/client/route.js
@@ -3,9 +3,7 @@ import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { t } from '../../utils';
-const dynamic = () => {
- import('./views');
-};
+const dynamic = () => import('./views');
FlowRouter.route('/admin/integrations', {
name: 'admin-integrations',
diff --git a/app/integrations/client/views/integrationsIncoming.html b/app/integrations/client/views/integrationsIncoming.html
index 6fef78f2712e0..d9aae546f7004 100644
--- a/app/integrations/client/views/integrationsIncoming.html
+++ b/app/integrations/client/views/integrationsIncoming.html
@@ -124,7 +124,7 @@
- {{#nrr nrrargs 'message' msg=exampleMsg settings=exampleSettings u=exampleUser}}{{/nrr}}
+ {{> message msg=exampleMsg settings=exampleSettings u=exampleUser}}
diff --git a/app/integrations/client/views/integrationsOutgoing.html b/app/integrations/client/views/integrationsOutgoing.html
index 50ef61c66880a..18d6fc8d5791f 100644
--- a/app/integrations/client/views/integrationsOutgoing.html
+++ b/app/integrations/client/views/integrationsOutgoing.html
@@ -171,7 +171,7 @@
- {{#nrr nrrargs 'message' msg=exampleMsg settings=exampleSettings u=exampleUser}}{{/nrr}}
+ {{> message msg=exampleMsg settings=exampleSettings u=exampleUser}}
{{/if}}
diff --git a/app/integrations/server/api/api.js b/app/integrations/server/api/api.js
index f645ea574e7b9..db79dc0943bdf 100644
--- a/app/integrations/server/api/api.js
+++ b/app/integrations/server/api/api.js
@@ -333,7 +333,7 @@ class WebHookAPI extends APIClass {
There is only one generic route propagated to Restivus which has URL-path-parameters for the integration and the token.
Since the rate-limiter operates on absolute routes, we need to add a limiter to the absolute url before we can validate it
*/
- enforceRateLimit(objectForRateLimitMatch, request, response) {
+ enforceRateLimit(objectForRateLimitMatch, request, response, userId) {
const { method, url } = request;
const route = url.replace(`/${ this.apiPath }`, '');
const nameRoute = this.getFullRouteName(route, [method.toLowerCase()]);
@@ -354,7 +354,7 @@ class WebHookAPI extends APIClass {
const integrationForRateLimitMatch = objectForRateLimitMatch;
integrationForRateLimitMatch.route = nameRoute;
- super.enforceRateLimit(integrationForRateLimitMatch, request, response);
+ super.enforceRateLimit(integrationForRateLimitMatch, request, response, userId);
}
}
diff --git a/app/integrations/server/index.js b/app/integrations/server/index.js
index 65e7e01cb60a1..b022e3034fea1 100644
--- a/app/integrations/server/index.js
+++ b/app/integrations/server/index.js
@@ -1,8 +1,6 @@
import '../lib/rocketchat';
import './logger';
import './lib/validation';
-import './publications/integrations';
-import './publications/integrationHistory';
import './methods/incoming/addIncomingIntegration';
import './methods/incoming/updateIncomingIntegration';
import './methods/incoming/deleteIncomingIntegration';
diff --git a/app/integrations/server/lib/triggerHandler.js b/app/integrations/server/lib/triggerHandler.js
index 866a5a10166b9..04c4227fa0230 100644
--- a/app/integrations/server/lib/triggerHandler.js
+++ b/app/integrations/server/lib/triggerHandler.js
@@ -491,22 +491,8 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler {
}
}
- executeTriggers(...args) {
- logger.outgoing.debug('Execute Trigger:', args[0]);
-
- const argObject = this.eventNameArgumentsToObject(...args);
- const { event, message, room } = argObject;
-
- // Each type of event should have an event and a room attached, otherwise we
- // wouldn't know how to handle the trigger nor would we have anywhere to send the
- // result of the integration
- if (!event) {
- return;
- }
-
+ getTriggersToExecute(room, message) {
const triggersToExecute = [];
-
- logger.outgoing.debug('Starting search for triggers for the room:', room ? room._id : '__any');
if (room) {
switch (room.t) {
case 'd':
@@ -573,6 +559,25 @@ integrations.triggerHandler = new class RocketChatIntegrationHandler {
break;
}
}
+ return triggersToExecute;
+ }
+
+ executeTriggers(...args) {
+ logger.outgoing.debug('Execute Trigger:', args[0]);
+
+ const argObject = this.eventNameArgumentsToObject(...args);
+ const { event, message, room } = argObject;
+
+ // Each type of event should have an event and a room attached, otherwise we
+ // wouldn't know how to handle the trigger nor would we have anywhere to send the
+ // result of the integration
+ if (!event) {
+ return;
+ }
+
+ logger.outgoing.debug('Starting search for triggers for the room:', room ? room._id : '__any');
+
+ const triggersToExecute = this.getTriggersToExecute(room, message);
if (this.triggers.__any) {
// For outgoing integration which don't rely on rooms.
diff --git a/app/integrations/server/publications/integrationHistory.js b/app/integrations/server/publications/integrationHistory.js
deleted file mode 100644
index cc2ea926a2354..0000000000000
--- a/app/integrations/server/publications/integrationHistory.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasAtLeastOnePermission } from '../../../authorization/server';
-import { IntegrationHistory } from '../../../models/server';
-import { mountIntegrationHistoryQueryBasedOnPermissions } from '../lib/mountQueriesBasedOnPermission';
-
-Meteor.publish('integrationHistory', function _integrationHistoryPublication(integrationId, limit = 25) {
- console.warn('The publication "integrationHistory" is deprecated and will be removed after version v3.0.0');
- if (!this.userId) {
- return this.ready();
- }
- if (!hasAtLeastOnePermission(this.userId, [
- 'manage-outgoing-integrations',
- 'manage-own-outgoing-integrations',
- ])) {
- throw new Meteor.Error('not-authorized');
- }
-
- return IntegrationHistory.find(Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(this.userId, integrationId)), { sort: { _updatedAt: -1 }, limit });
-});
diff --git a/app/integrations/server/publications/integrations.js b/app/integrations/server/publications/integrations.js
deleted file mode 100644
index 48ebe040b380f..0000000000000
--- a/app/integrations/server/publications/integrations.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasAtLeastOnePermission } from '../../../authorization/server';
-import { Integrations } from '../../../models/server';
-import { mountIntegrationQueryBasedOnPermissions } from '../lib/mountQueriesBasedOnPermission';
-
-Meteor.publish('integrations', function _integrationPublication() {
- console.warn('The publication "integrations" is deprecated and will be removed after version v3.0.0');
- if (!this.userId) {
- return this.ready();
- }
-
- if (!hasAtLeastOnePermission(this.userId, [
- 'manage-outgoing-integrations',
- 'manage-own-outgoing-integrations',
- 'manage-incoming-integrations',
- 'manage-own-incoming-integrations',
- ])) {
- throw new Meteor.Error('not-authorized');
- }
-
- return Integrations.find(mountIntegrationQueryBasedOnPermissions(this.userId));
-});
diff --git a/app/katex/katex.min.css b/app/katex/katex.min.css
index 631f6c1d67468..c0cd1451ae6df 100644
--- a/app/katex/katex.min.css
+++ b/app/katex/katex.min.css
@@ -1 +1 @@
-@font-face{font-family:KaTeX_AMS;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_SansSerif;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_SansSerif;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_SansSerif;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Script;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size1;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size2;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size3;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size4;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Typewriter;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype");font-weight:400;font-style:normal}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:inline-block;text-align:initial}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;white-space:nowrap;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-html{display:inline-block}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .base{position:relative}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathit{font-family:KaTeX_Math;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr{font-family:KaTeX_Script}.katex .mathsf{font-family:KaTeX_SansSerif}.katex .mainit{font-family:KaTeX_Main;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%}.katex .mspace{display:inline-block}.katex .mspace.negativethinspace{margin-left:-.16667em}.katex .mspace.muspace{width:.055556em}.katex .mspace.thinspace{width:.16667em}.katex .mspace.negativemediumspace{margin-left:-.22222em}.katex .mspace.mediumspace{width:.22222em}.katex .mspace.thickspace{width:.27778em}.katex .mspace.sixmuspace{width:.333333em}.katex .mspace.eightmuspace{width:.444444em}.katex .mspace.enspace{width:.5em}.katex .mspace.twelvemuspace{width:.666667em}.katex .mspace.quad{width:1em}.katex .mspace.qquad{width:2em}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer,.katex .sizing{display:inline-block}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{width:0;position:relative}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;margin:0 -.125em;width:.25em;overflow:hidden;position:relative}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex .vertical-separator svg{width:.25em}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox{box-sizing:border-box;border:.04em solid #000}.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap+.mbin,.katex .cancel-lap+.mord,.katex .cancel-lap+.msupsub,.katex .mbin+.cancel-lap,.katex .mord+.cancel-lap{margin-left:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}
\ No newline at end of file
+@font-face{font-family:KaTeX_AMS;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Caligraphic;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Fraktur;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:KaTeX_Main;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype");font-weight:700;font-style:italic}@font-face{font-family:KaTeX_Math;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype");font-weight:700;font-style:normal}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype");font-weight:400;font-style:italic}@font-face{font-family:"KaTeX_SansSerif";src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Script;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size1;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size2;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size3;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Size4;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype");font-weight:400;font-style:normal}@font-face{font-family:KaTeX_Typewriter;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype");font-weight:400;font-style:normal}.katex{font:normal 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important}.katex .katex-version:after{content:"0.11.1"}.katex .katex-mathml{position:absolute;clip:rect(1px,1px,1px,1px);padding:0;border:0;height:1px;width:1px;overflow:hidden}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathdefault{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-weight:700;font-style:italic}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;vertical-align:bottom;position:relative}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;vertical-align:bottom;font-size:1px;width:2px;min-width:2px}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{width:0;position:relative}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{display:inline-block;border:0 solid;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{display:inline-block;width:100%;border-bottom-style:solid}.katex .hdashline{display:inline-block;width:100%;border-bottom-style:dashed}.katex .sqrt>.root{margin-left:.27777778em;margin-right:-.55555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.83333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.16666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.66666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.45666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.14666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.71428571em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.85714286em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.14285714em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.28571429em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.42857143em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.71428571em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.05714286em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.46857143em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.96285714em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.55428571em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.55555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.66666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.77777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.88888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.11111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.33333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.30444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.76444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.41666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.58333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.66666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.83333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.72833333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.07333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.34722222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.41666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.48611111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.55555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.69444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.83333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.44027778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.72777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.28935185em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.34722222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.40509259em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.46296296em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.52083333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.69444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.83333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.20023148em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.43981481em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.24108004em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.28929605em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.33751205em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.38572806em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.43394407em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.48216008em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.57859209em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.69431051em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.83317261em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.19961427em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.20096463em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.24115756em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.28135048em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.32154341em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.36173633em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.40192926em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.48231511em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.57877814em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.69453376em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.83360129em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .op-limits>.vlist-t{text-align:center}.katex .accent>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{display:block;position:absolute;width:100%;height:inherit;fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex svg path{stroke:none}.katex img{border-style:none;min-width:0;min-height:0;max-width:none;max-height:none}.katex .stretchy{width:100%;display:block;position:relative;overflow:hidden}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{width:100%;position:relative;overflow:hidden}.katex .halfarrow-left{position:absolute;left:0;width:50.2%;overflow:hidden}.katex .halfarrow-right{position:absolute;right:0;width:50.2%;overflow:hidden}.katex .brace-left{position:absolute;left:0;width:25.1%;overflow:hidden}.katex .brace-center{position:absolute;left:25%;width:50%;overflow:hidden}.katex .brace-right{position:absolute;right:0;width:25.1%;overflow:hidden}.katex .x-arrow-pad{padding:0 .5em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{box-sizing:border-box;border:.04em solid}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{text-align:left}
diff --git a/app/lazy-load/client/index.js b/app/lazy-load/client/index.js
index 248ff4f3bc698..61a0ece9c7354 100644
--- a/app/lazy-load/client/index.js
+++ b/app/lazy-load/client/index.js
@@ -1,43 +1,46 @@
-import { Blaze } from 'meteor/blaze';
-import _ from 'underscore';
import './lazyloadImage';
-const getEl = (el, instance) => (instance && instance.firstNode) || el;
+const map = new WeakMap();
-const loadImage = (el, instance) => {
- const element = getEl(el, instance);
+const featureExists = !!window.IntersectionObserver;
+const loadImage = (el) => {
+ const instance = map.get(el);
+
+ map.delete(el);
+
+ if (!instance) {
+ return instance.loaded.set(true);
+ }
const img = new Image();
- const src = element.getAttribute('data-src');
+ const src = el.getAttribute('data-src');
img.onload = () => {
- if (instance) {
- instance.loaded.set(true);
- } else {
- element.className = element.className.replace('lazy-img', '');
- element.src = src;
- }
- element.removeAttribute('data-src');
+ el.className = el.className.replace('lazy-img', '');
+ el.src = src;
+ el.removeAttribute('data-src');
};
img.src = src;
};
-const isVisible = (el, instance) => {
- requestAnimationFrame(() => {
- const rect = getEl(el, instance).getBoundingClientRect();
- if (rect.top >= -100 && rect.left >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight)) {
- return loadImage(el, instance);
+const observer = featureExists && new IntersectionObserver(
+ (entries, observer) => entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ observer.unobserve(entry.target);
+ return loadImage(entry.target);
}
- });
-};
+ })
+ ,
+ {
+ threshold: [0],
+ trackVisibility: true,
+ delay: 230,
+ },
+);
-window.addEventListener('resize', window.lazyloadtick);
-
-export const lazyloadtick = _.debounce(() => {
- const lazyImg = document.querySelectorAll('.lazy-img[data-src]');
- Array.from(lazyImg).forEach((el) =>
- isVisible(el, Blaze.getView(el)._templateInstance),
- );
-}, 300);
-
-window.lazyloadtick = lazyloadtick;
-
-export const addImage = (instance) => isVisible(instance.firstNode, instance);
+export const addImage = (instance) => {
+ const el = instance.firstNode;
+ map.set(el, instance);
+ if (featureExists) {
+ return observer.observe(el);
+ }
+ loadImage(el);
+};
diff --git a/app/lib/lib/MessageTypes.js b/app/lib/lib/MessageTypes.js
index 012d76f0360cf..3f6df60904161 100644
--- a/app/lib/lib/MessageTypes.js
+++ b/app/lib/lib/MessageTypes.js
@@ -160,3 +160,46 @@ Meteor.startup(function() {
},
});
});
+
+export const MessageTypesValues = [
+ {
+ key: 'uj',
+ i18nLabel: 'Message_HideType_uj',
+ }, {
+ key: 'ul',
+ i18nLabel: 'Message_HideType_ul',
+ }, {
+ key: 'ru',
+ i18nLabel: 'Message_HideType_ru',
+ }, {
+ key: 'au',
+ i18nLabel: 'Message_HideType_au',
+ }, {
+ key: 'mute_unmute',
+ i18nLabel: 'Message_HideType_mute_unmute',
+ }, {
+ key: 'r',
+ i18nLabel: 'Message_HideType_r',
+ }, {
+ key: 'ut',
+ i18nLabel: 'Message_HideType_ut',
+ }, {
+ key: 'wm',
+ i18nLabel: 'Message_HideType_wm',
+ }, {
+ key: 'rm',
+ i18nLabel: 'Message_HideType_rm',
+ }, {
+ key: 'subscription_role_added',
+ i18nLabel: 'Message_HideType_subscription_role_added',
+ }, {
+ key: 'subscription_role_removed',
+ i18nLabel: 'Message_HideType_subscription_role_removed',
+ }, {
+ key: 'room_archived',
+ i18nLabel: 'Message_HideType_room_archived',
+ }, {
+ key: 'room_unarchived',
+ i18nLabel: 'Message_HideType_room_unarchived',
+ },
+];
diff --git a/app/lib/lib/roomTypes/private.js b/app/lib/lib/roomTypes/private.js
index ee095a937f2a3..a418a4d436fc8 100644
--- a/app/lib/lib/roomTypes/private.js
+++ b/app/lib/lib/roomTypes/private.js
@@ -90,9 +90,9 @@ export class PrivateRoomType extends RoomTypeConfig {
return !room.broadcast;
case RoomSettingsEnum.REACT_WHEN_READ_ONLY:
return !room.broadcast && room.ro;
- case RoomSettingsEnum.SYSTEM_MESSAGES:
case RoomSettingsEnum.E2E:
return settings.get('E2E_Enable') === true;
+ case RoomSettingsEnum.SYSTEM_MESSAGES:
default:
return true;
}
diff --git a/app/lib/server/functions/createRoom.js b/app/lib/server/functions/createRoom.js
index 116c6f5e92dcc..434ecc8ac0862 100644
--- a/app/lib/server/functions/createRoom.js
+++ b/app/lib/server/functions/createRoom.js
@@ -57,7 +57,6 @@ export const createRoom = function(type, name, owner, members, readOnly, extraDa
}, extraData, {
ts: now,
ro: readOnly === true,
- sysMes: readOnly !== true,
});
if (type === 'd') {
diff --git a/app/lib/server/functions/deleteMessage.js b/app/lib/server/functions/deleteMessage.js
index e74ff6d7d1bbf..4e80b52fb1147 100644
--- a/app/lib/server/functions/deleteMessage.js
+++ b/app/lib/server/functions/deleteMessage.js
@@ -1,10 +1,10 @@
import { Meteor } from 'meteor/meteor';
-import { FileUpload } from '../../../file-upload';
-import { settings } from '../../../settings';
-import { Messages, Uploads, Rooms } from '../../../models';
-import { Notifications } from '../../../notifications';
-import { callbacks } from '../../../callbacks';
+import { FileUpload } from '../../../file-upload/server';
+import { settings } from '../../../settings/server';
+import { Messages, Uploads, Rooms } from '../../../models/server';
+import { Notifications } from '../../../notifications/server';
+import { callbacks } from '../../../callbacks/server';
import { Apps } from '../../../apps/server';
export const deleteMessage = function(message, user) {
diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js
index 031ce3c612eb4..4962c5522f064 100644
--- a/app/lib/server/functions/deleteUser.js
+++ b/app/lib/server/functions/deleteUser.js
@@ -12,6 +12,10 @@ export const deleteUser = function(userId) {
fields: { username: 1, avatarOrigin: 1, federation: 1 },
});
+ if (!user) {
+ return;
+ }
+
if (user.federation) {
const existingSubscriptions = Subscriptions.find({ 'u._id': user._id }).count();
diff --git a/app/lib/server/functions/getAvatarSuggestionForUser.js b/app/lib/server/functions/getAvatarSuggestionForUser.js
index 2960145bfb9db..f2bb8cd473065 100644
--- a/app/lib/server/functions/getAvatarSuggestionForUser.js
+++ b/app/lib/server/functions/getAvatarSuggestionForUser.js
@@ -5,97 +5,132 @@ import { ServiceConfiguration } from 'meteor/service-configuration';
import { settings } from '../../../settings';
-export function getAvatarSuggestionForUser(user) {
- check(user, Object);
-
- const avatars = [];
-
- if (user.services && user.services.facebook && user.services.facebook.id && settings.get('Accounts_OAuth_Facebook')) {
- avatars.push({
- service: 'facebook',
- url: `https://graph.facebook.com/${ user.services.facebook.id }/picture?type=large`,
- });
- }
-
- if (user.services && user.services.google && user.services.google.picture && user.services.google.picture !== 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg' && settings.get('Accounts_OAuth_Google')) {
- avatars.push({
- service: 'google',
- url: user.services.google.picture,
- });
- }
-
- if (user.services && user.services.github && user.services.github.username && settings.get('Accounts_OAuth_Github')) {
- avatars.push({
- service: 'github',
- url: `https://avatars.githubusercontent.com/${ user.services.github.username }?s=200`,
- });
- }
-
- if (user.services && user.services.linkedin && user.services.linkedin.pictureUrl && settings.get('Accounts_OAuth_Linkedin')) {
- avatars.push({
- service: 'linkedin',
- url: user.services.linkedin.pictureUrl,
- });
- }
-
- if (user.services && user.services.twitter && user.services.twitter.profile_image_url_https && settings.get('Accounts_OAuth_Twitter')) {
- avatars.push({
- service: 'twitter',
- url: user.services.twitter.profile_image_url_https.replace(/_normal|_bigger/, ''),
- });
- }
-
- if (user.services && user.services.gitlab && user.services.gitlab.avatar_url && settings.get('Accounts_OAuth_Gitlab')) {
- avatars.push({
- service: 'gitlab',
- url: user.services.gitlab.avatar_url,
- });
- }
-
- if (user.services && user.services.blockstack && user.services.blockstack.image && settings.get('Blockstack_Enable')) {
- avatars.push({
- service: 'blockstack',
- url: user.services.blockstack.image,
- });
- }
-
- for (const service in user.services) {
- if (user.services[service]._OAuthCustom) {
- const services = ServiceConfiguration.configurations.find({ service }, { fields: { secret: 0 } }).fetch();
+const avatarProviders = {
+ facebook(user) {
+ if (user.services && user.services.facebook && user.services.facebook.id && settings.get('Accounts_OAuth_Facebook')) {
+ return {
+ service: 'facebook',
+ url: `https://graph.facebook.com/${ user.services.facebook.id }/picture?type=large`,
+ };
+ }
+ },
+
+ google(user) {
+ if (user.services && user.services.google && user.services.google.picture && user.services.google.picture !== 'https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg' && settings.get('Accounts_OAuth_Google')) {
+ return {
+ service: 'google',
+ url: user.services.google.picture,
+ };
+ }
+ },
+
+ github(user) {
+ if (user.services && user.services.github && user.services.github.username && settings.get('Accounts_OAuth_Github')) {
+ return {
+ service: 'github',
+ url: `https://avatars.githubusercontent.com/${ user.services.github.username }?s=200`,
+ };
+ }
+ },
+
+ linkedin(user) {
+ if (user.services && user.services.linkedin && user.services.linkedin.pictureUrl && settings.get('Accounts_OAuth_Linkedin')) {
+ return {
+ service: 'linkedin',
+ url: user.services.linkedin.pictureUrl,
+ };
+ }
+ },
+
+ twitter(user) {
+ if (user.services && user.services.twitter && user.services.twitter.profile_image_url_https && settings.get('Accounts_OAuth_Twitter')) {
+ return {
+ service: 'twitter',
+ url: user.services.twitter.profile_image_url_https.replace(/_normal|_bigger/, ''),
+ };
+ }
+ },
+
+ gitlab(user) {
+ if (user.services && user.services.gitlab && user.services.gitlab.avatar_url && settings.get('Accounts_OAuth_Gitlab')) {
+ return {
+ service: 'gitlab',
+ url: user.services.gitlab.avatar_url,
+ };
+ }
+ },
+
+ blockstack(user) {
+ if (user.services && user.services.blockstack && user.services.blockstack.image && settings.get('Blockstack_Enable')) {
+ return {
+ service: 'blockstack',
+ url: user.services.blockstack.image,
+ };
+ }
+ },
+
+ customOAuth(user) {
+ const avatars = [];
+ for (const service in user.services) {
+ if (user.services[service]._OAuthCustom) {
+ const services = ServiceConfiguration.configurations.find({ service }, { fields: { secret: 0 } }).fetch();
+
+ if (services.length > 0) {
+ if (user.services[service].avatarUrl) {
+ avatars.push({
+ service,
+ url: user.services[service].avatarUrl,
+ });
+ }
+ }
+ }
+ }
+ return avatars;
+ },
+
+ emails(user) {
+ const avatars = [];
+ if (user.emails && user.emails.length > 0) {
+ for (const email of user.emails) {
+ if (email.verified === true) {
+ avatars.push({
+ service: 'gravatar',
+ url: Gravatar.imageUrl(email.address, {
+ default: '404',
+ size: 200,
+ secure: true,
+ }),
+ });
+ }
- if (services.length > 0) {
- if (user.services[service].avatarUrl) {
+ if (email.verified !== true) {
avatars.push({
- service,
- url: user.services[service].avatarUrl,
+ service: 'gravatar',
+ url: Gravatar.imageUrl(email.address, {
+ default: '404',
+ size: 200,
+ secure: true,
+ }),
});
}
}
}
- }
+ return avatars;
+ },
+};
- if (user.emails && user.emails.length > 0) {
- for (const email of user.emails) {
- if (email.verified === true) {
- avatars.push({
- service: 'gravatar',
- url: Gravatar.imageUrl(email.address, {
- default: '404',
- size: 200,
- secure: true,
- }),
- });
- }
+export function getAvatarSuggestionForUser(user) {
+ check(user, Object);
+
+ const avatars = [];
- if (email.verified !== true) {
- avatars.push({
- service: 'gravatar',
- url: Gravatar.imageUrl(email.address, {
- default: '404',
- size: 200,
- secure: true,
- }),
- });
+ for (const avatarProvider of Object.values(avatarProviders)) {
+ const avatar = avatarProvider(user);
+ if (avatar) {
+ if (Array.isArray(avatar)) {
+ avatars.push(...avatar);
+ } else {
+ avatars.push(avatar);
}
}
}
diff --git a/app/lib/server/functions/insertMessage.js b/app/lib/server/functions/insertMessage.js
index 35c7ccec1501d..d45b8dcc275d7 100644
--- a/app/lib/server/functions/insertMessage.js
+++ b/app/lib/server/functions/insertMessage.js
@@ -47,7 +47,7 @@ const validateAttachment = (attachment) => {
check(attachment, objectMaybeIncluding({
color: String,
text: String,
- ts: Match.OneOf(String, Match.Integer),
+ ts: Match.OneOf(String, Number),
thumb_url: String,
button_alignment: String,
actions: [Match.Any],
diff --git a/app/lib/server/functions/loadMessageHistory.js b/app/lib/server/functions/loadMessageHistory.js
index 6625128440005..8e381a3fadc16 100644
--- a/app/lib/server/functions/loadMessageHistory.js
+++ b/app/lib/server/functions/loadMessageHistory.js
@@ -1,27 +1,19 @@
import { settings } from '../../../settings';
-import { Messages } from '../../../models';
+import { Messages, Rooms } from '../../../models';
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
-const hideMessagesOfType = [];
+const hideMessagesOfTypeServer = new Set();
-settings.get(/Message_HideType_.+/, function(key, value) {
- const type = key.replace('Message_HideType_', '');
- const types = type === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [type];
-
- return types.forEach((type) => {
- const index = hideMessagesOfType.indexOf(type);
-
- if (value === true && index === -1) {
- return hideMessagesOfType.push(type);
- }
-
- if (index > -1) {
- return hideMessagesOfType.splice(index, 1);
- }
- });
+settings.get('Hide_System_Messages', function(key, values) {
+ const hiddenTypes = values.reduce((array, value) => [...array, ...value === 'mute_unmute' ? ['user-muted', 'user-unmuted'] : [value]], []);
+ hideMessagesOfTypeServer.clear();
+ hiddenTypes.forEach((item) => hideMessagesOfTypeServer.add(item));
});
export const loadMessageHistory = function loadMessageHistory({ userId, rid, end, limit = 20, ls }) {
+ const room = Rooms.findOne(rid, { fields: { sysMes: 1 } });
+
+ const hiddenMessageTypes = Array.isArray(room && room.sysMes) ? room.sysMes : Array.from(hideMessagesOfTypeServer.values()); // TODO probably remove on chained event system
const options = {
sort: {
ts: -1,
@@ -35,12 +27,7 @@ export const loadMessageHistory = function loadMessageHistory({ userId, rid, end
};
}
- let records;
- if (end != null) {
- records = Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes(rid, end, hideMessagesOfType, options).fetch();
- } else {
- records = Messages.findVisibleByRoomIdNotContainingTypes(rid, hideMessagesOfType, options).fetch();
- }
+ const records = end != null ? Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes(rid, end, hiddenMessageTypes, options).fetch() : Messages.findVisibleByRoomIdNotContainingTypes(rid, hiddenMessageTypes, options).fetch();
const messages = normalizeMessagesForUser(records, userId);
let unreadNotLoaded = 0;
let firstUnread;
@@ -50,8 +37,7 @@ export const loadMessageHistory = function loadMessageHistory({ userId, rid, end
if ((firstMessage != null ? firstMessage.ts : undefined) > ls) {
delete options.limit;
-
- const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(rid, ls, firstMessage.ts, hideMessagesOfType, {
+ const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(rid, ls, firstMessage.ts, hiddenMessageTypes, {
limit: 1,
sort: {
ts: 1,
diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js
index 7e0caef79a9d5..ea5ce3088d51c 100644
--- a/app/lib/server/functions/sendMessage.js
+++ b/app/lib/server/functions/sendMessage.js
@@ -90,7 +90,7 @@ const validateAttachment = (attachment) => {
check(attachment, objectMaybeIncluding({
color: String,
text: String,
- ts: Match.OneOf(String, Match.Integer),
+ ts: Match.OneOf(String, Number),
thumb_url: ValidFullURLParam,
button_alignment: String,
actions: [Match.Any],
diff --git a/app/lib/server/functions/setUserAvatar.js b/app/lib/server/functions/setUserAvatar.js
index 4e7ec6201c9bc..9cab632e239ac 100644
--- a/app/lib/server/functions/setUserAvatar.js
+++ b/app/lib/server/functions/setUserAvatar.js
@@ -17,6 +17,10 @@ export const setUserAvatar = function(user, dataURI, contentType, service) {
try {
result = HTTP.get(dataURI, { npmRequestOptions: { encoding: 'binary', rejectUnauthorized: false } });
+ if (!result) {
+ console.log(`Not a valid response, from the avatar url: ${ dataURI }`);
+ throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'setUserAvatar', url: dataURI });
+ }
} catch (error) {
if (!error.response || error.response.statusCode !== 404) {
console.log(`Error while handling the setting of the avatar from a url (${ dataURI }) for ${ user.username }:`, error);
@@ -26,12 +30,12 @@ export const setUserAvatar = function(user, dataURI, contentType, service) {
if (result.statusCode !== 200) {
console.log(`Not a valid response, ${ result.statusCode }, from the avatar url: ${ dataURI }`);
- throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'RocketChat.setUserAvatar', url: dataURI });
+ throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'setUserAvatar', url: dataURI });
}
if (!/image\/.+/.test(result.headers['content-type'])) {
console.log(`Not a valid content-type from the provided url, ${ result.headers['content-type'] }, from the avatar url: ${ dataURI }`);
- throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'RocketChat.setUserAvatar', url: dataURI });
+ throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${ dataURI }`, { function: 'setUserAvatar', url: dataURI });
}
encoding = 'binary';
diff --git a/app/lib/server/methods/executeSlashCommandPreview.js b/app/lib/server/methods/executeSlashCommandPreview.js
index 887721dcfb113..52f2c18527a5c 100644
--- a/app/lib/server/methods/executeSlashCommandPreview.js
+++ b/app/lib/server/methods/executeSlashCommandPreview.js
@@ -29,6 +29,6 @@ Meteor.methods({
});
}
- return slashCommands.executePreview(command.cmd, command.params, command.msg, preview);
+ return slashCommands.executePreview(command.cmd, command.params, command.msg, preview, command.triggerId);
},
});
diff --git a/app/lib/server/methods/saveSettings.js b/app/lib/server/methods/saveSettings.js
index d7cf7bd02ad6e..97c12bd85cac5 100644
--- a/app/lib/server/methods/saveSettings.js
+++ b/app/lib/server/methods/saveSettings.js
@@ -37,6 +37,9 @@ Meteor.methods({
case 'int':
check(value, Number);
break;
+ case 'multiSelect':
+ check(value, Array);
+ break;
default:
check(value, String);
break;
diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js
index 827ad31ba39c8..d25284a3639fa 100644
--- a/app/lib/server/methods/sendMessage.js
+++ b/app/lib/server/methods/sendMessage.js
@@ -50,6 +50,7 @@ export function executeSendMessage(uid, message) {
const user = Users.findOneById(uid, {
fields: {
username: 1,
+ type: 1,
...!!settings.get('Message_SetNameToAliasEnabled') && { name: 1 },
},
});
@@ -67,7 +68,7 @@ export function executeSendMessage(uid, message) {
}
try {
- const room = canSendMessage(rid, { uid, username: user.username });
+ const room = canSendMessage(rid, { uid, username: user.username, type: user.type });
if (message.alias == null && settings.get('Message_SetNameToAliasEnabled')) {
message.alias = user.name;
}
diff --git a/app/lib/server/startup/email.js b/app/lib/server/startup/email.js
index d31d4de65a59a..852618fd1a1cb 100644
--- a/app/lib/server/startup/email.js
+++ b/app/lib/server/startup/email.js
@@ -2,6 +2,10 @@ import { settings } from '../../../settings';
settings.addGroup('Email', function() {
this.section('Style', function() {
+ this.add('email_plain_text_only', false, {
+ type: 'boolean',
+ });
+
this.add('email_style', `html, body, .body { font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Helvetica Neue','Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Meiryo UI',Arial,sans-serif; }
body, .body {
@@ -103,6 +107,10 @@ settings.addGroup('Email', function() {
multiline: true,
i18nLabel: 'email_style_label',
i18nDescription: 'email_style_description',
+ enableQuery: {
+ _id: 'email_plain_text_only',
+ value: false,
+ },
});
});
diff --git a/app/lib/server/startup/rateLimiter.js b/app/lib/server/startup/rateLimiter.js
index eeda6db044112..f12f23caa8c85 100644
--- a/app/lib/server/startup/rateLimiter.js
+++ b/app/lib/server/startup/rateLimiter.js
@@ -5,6 +5,9 @@ import { RateLimiter } from 'meteor/rate-limit';
import { settings } from '../../../settings';
import { metrics } from '../../../metrics';
+import { Logger } from '../../../logger';
+
+const logger = new Logger('RateLimiter', {});
// Get initial set of names already registered for rules
const names = new Set(Object.values(DDPRateLimiter.printRules())
@@ -107,8 +110,8 @@ const ruleIds = {};
const callback = (message, name) => (reply, input) => {
if (reply.allowed === false) {
- console.warn('DDP RATE LIMIT:', message);
- console.warn(JSON.stringify({ ...reply, ...input }, null, 2));
+ logger.info('DDP RATE LIMIT:', message);
+ logger.info(JSON.stringify({ ...reply, ...input }, null, 2));
metrics.ddpRateLimitExceeded.inc({
limit_name: name,
user_id: input.userId,
diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js
index 7808e707e5885..8bd5252c29787 100644
--- a/app/lib/server/startup/settings.js
+++ b/app/lib/server/startup/settings.js
@@ -2,6 +2,7 @@ import { Random } from 'meteor/random';
import { settings } from '../../../settings';
import './email';
+import { MessageTypesValues } from '../../lib/MessageTypes';
// Insert server unique id if it doesn't exist
settings.add('uniqueID', process.env.DEPLOYMENT_ID || Random.id(), {
@@ -440,11 +441,6 @@ settings.addGroup('Accounts', function() {
public: true,
i18nLabel: 'Email_Notification_Mode',
});
- this.add('Accounts_Default_User_Preferences_roomCounterSidebar', false, {
- type: 'boolean',
- public: true,
- i18nLabel: 'Show_room_counter_on_sidebar',
- });
this.add('Accounts_Default_User_Preferences_newRoomNotification', 'door', {
type: 'select',
values: [
@@ -1098,28 +1094,15 @@ settings.addGroup('Message', function() {
type: 'int',
public: true,
});
- this.add('Message_HideType_uj', false, {
- type: 'boolean',
- public: true,
- });
- this.add('Message_HideType_ul', false, {
- type: 'boolean',
- public: true,
- });
- this.add('Message_HideType_ru', false, {
- type: 'boolean',
- public: true,
- });
- this.add('Message_HideType_au', false, {
- type: 'boolean',
- public: true,
- });
- this.add('Message_HideType_mute_unmute', false, {
- type: 'boolean',
+
+ this.add('Hide_System_Messages', [], {
+ type: 'multiSelect',
public: true,
+ values: MessageTypesValues,
});
+
this.add('Message_ErasureType', 'Delete', {
type: 'select',
public: true,
@@ -1390,6 +1373,10 @@ settings.addGroup('Layout', function() {
type: 'boolean',
public: true,
});
+ return this.add('UI_Show_top_navbar_embedded_layout', false, {
+ type: 'boolean',
+ public: true,
+ });
});
});
diff --git a/app/livechat/client/views/app/livechatCustomFieldForm.html b/app/livechat/client/views/app/livechatCustomFieldForm.html
index e90f9c4ab9738..d879ce74218c2 100644
--- a/app/livechat/client/views/app/livechatCustomFieldForm.html
+++ b/app/livechat/client/views/app/livechatCustomFieldForm.html
@@ -34,6 +34,12 @@
+
diff --git a/app/livechat/client/views/app/tabbar/agentInfo.js b/app/livechat/client/views/app/tabbar/agentInfo.js
index 8d5d04a63e7fe..6894586f32dac 100644
--- a/app/livechat/client/views/app/tabbar/agentInfo.js
+++ b/app/livechat/client/views/app/tabbar/agentInfo.js
@@ -20,7 +20,9 @@ Template.agentInfo.helpers({
const hasCustomFields = customFieldsTemplate() !== null;
return (availableDepartments.length > 0 && hasPermission('add-livechat-department-agents')) || hasCustomFields;
},
-
+ uid() {
+ return Template.instance().agent.get()._id;
+ },
name() {
const agent = Template.instance().agent.get();
return agent && agent.name ? agent.name : TAPi18n.__('Unnamed');
diff --git a/app/livechat/client/views/app/tabbar/visitorEdit.html b/app/livechat/client/views/app/tabbar/visitorEdit.html
index 9f8510b53bfc4..0d3afb3118bec 100644
--- a/app/livechat/client/views/app/tabbar/visitorEdit.html
+++ b/app/livechat/client/views/app/tabbar/visitorEdit.html
@@ -30,6 +30,17 @@
{{username}}
+
+ {{#each visitorCustomFields}}
+