- {{#if latest.iconFileData}}
-
+ {{#if app.iconFileData}}
+
{{else}}
-
+
{{/if}}
- {{latest.name}}
+ {{app.name}}
- {{#if latest.author.name}}
- by {{latest.author.name}}
+ {{#if app.author.name}}
+ by {{app.author.name}}
{{/if}}
@@ -69,19 +70,19 @@
- {{#if latest.summary}}
- {{latest.summary}}
+ {{#if app.summary}}
+ {{app.summary}}
{{else}}
- {{latest.description}}
+ {{app.description}}
{{/if}}
- {{#if latest.summary}}
+ {{#if app.summary}}
- {{latest.description}}
+ {{app.description}}
{{/if}}
- {{#each category in latest.categories}}
+ {{#each category in app.categories}}
{{category}}
{{/each}}
@@ -92,54 +93,40 @@
- {{purchaseTypeDisplay .}}
+ {{purchaseTypeDisplay app}}
- {{priceDisplay .}}
+ {{priceDisplay app}}
|
- {{#if isInstalled .}}
- {{#if canUpdate .}}
-
|
diff --git a/app/apps/client/admin/marketplace.js b/app/apps/client/admin/marketplace.js
index 5e447fe39445..0907080453d2 100644
--- a/app/apps/client/admin/marketplace.js
+++ b/app/apps/client/admin/marketplace.js
@@ -1,540 +1,333 @@
-import toastr from 'toastr';
-import { Meteor } from 'meteor/meteor';
-import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
+import { ReactiveDict } from 'meteor/reactive-dict';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
-import semver from 'semver';
-import { settings } from '../../../settings';
-import { t, APIClient } from '../../../utils';
-import { modal } from '../../../ui-utils';
+import { SideNav, call } from '../../../ui-utils/client';
+import { t } from '../../../utils';
import { AppEvents } from '../communication';
import { Apps } from '../orchestrator';
-import { SideNav, popover } from '../../../ui-utils/client';
+import {
+ appButtonProps,
+ appStatusSpanProps,
+ checkCloudLogin,
+ formatPrice,
+ formatPricingPlan,
+ handleAPIError,
+ promptSubscription,
+ triggerAppPopoverMenu,
+ warnStatusChange,
+} from './helpers';
import './marketplace.html';
-import './marketplace.css';
-const ENABLED_STATUS = ['auto_enabled', 'manually_enabled'];
-const enabled = ({ status }) => ENABLED_STATUS.includes(status);
-const sortByColumn = (array, column, inverted) =>
- array.sort((a, b) => {
- if (a.latest[column] < b.latest[column] && !inverted) {
- return -1;
- }
- return 1;
+Template.marketplace.onCreated(function() {
+ this.state = new ReactiveDict({
+ isLoggedInCloud: true,
+ apps: [], // TODO: maybe use another ReactiveDict here
+ isLoading: true,
+ searchText: '',
+ sortedColumn: 'name',
+ isAscendingOrder: true,
+
+ // TODO: to use these fields
+ page: 0,
+ itemsPerPage: 0,
+ wasEndReached: false,
});
-const getCloudLoggedIn = async (instance) => {
- Meteor.call('cloud:checkUserLoggedIn', (error, result) => {
- if (error) {
- console.warn(error);
- return;
- }
-
- instance.cloudLoggedIn.set(result);
- });
-};
-
-const handleAPIError = (e, instance) => {
- console.error(e);
- const errMsg = (e.xhr.responseJSON && e.xhr.responseJSON.error) || e.message;
- toastr.error(errMsg);
-
- if (errMsg === 'Unauthorized') {
- getCloudLoggedIn(instance);
- }
-};
-
-const getApps = async (instance) => {
- instance.isLoading.set(true);
-
- try {
- const data = await APIClient.get('apps?marketplace=true');
-
- instance.apps.set(data);
- } catch (e) {
- handleAPIError(e, instance);
- }
-
- instance.isLoading.set(false);
- instance.ready.set(true);
-};
-
-const getInstalledApps = async (instance) => {
- try {
- const data = await APIClient.get('apps');
- const apps = data.apps.map((app) => ({ latest: app }));
- instance.installedApps.set(apps);
- } catch (e) {
- handleAPIError(e, instance);
- }
-};
-
-const formatPrice = (price) => `\$${ Number.parseFloat(price).toFixed(2) }`;
-
-const formatPricingPlan = (pricingPlan) => {
- const perUser = pricingPlan.isPerSeat && pricingPlan.tiers && pricingPlan.tiers.length;
-
- const pricingPlanTranslationString = [
- 'Apps_Marketplace_pricingPlan',
- pricingPlan.strategy,
- perUser && 'perUser',
- ].filter(Boolean).join('_');
-
- return t(pricingPlanTranslationString, {
- price: formatPrice(pricingPlan.price),
- });
-};
-
-const isLoggedInCloud = (instance) => {
- if (instance.cloudLoggedIn.get()) {
- return true;
- }
-
- modal.open({
- title: t('Apps_Marketplace_Login_Required_Title'),
- text: t('Apps_Marketplace_Login_Required_Description'),
- type: 'info',
- showCancelButton: true,
- confirmButtonColor: '#DD6B55',
- confirmButtonText: t('Login'),
- cancelButtonText: t('Cancel'),
- closeOnConfirm: true,
- html: false,
- }, (confirmed) => {
- if (confirmed) {
- FlowRouter.go('/admin/cloud');
+ (async () => {
+ try {
+ this.state.set('isLoggedInCloud', await call('cloud:checkUserLoggedIn'));
+ } catch (error) {
+ handleAPIError(error);
}
- });
- return false;
-};
+ try {
+ const appsFromMarketplace = await Apps.getAppsFromMarketplace();
+ const installedApps = await Apps.getApps();
+
+ const apps = appsFromMarketplace.map((app) => {
+ const installedApp = installedApps.find(({ id }) => id === app.id);
+
+ if (!installedApp) {
+ return {
+ ...app,
+ status: undefined,
+ marketplaceVersion: app.version,
+ };
+ }
+
+ return {
+ ...app,
+ installed: true,
+ status: installedApp.status,
+ version: installedApp.version,
+ marketplaceVersion: app.version,
+ };
+ });
-const triggerButtonLoadingState = (button) => {
- const icon = button.querySelector('.rc-icon use');
- const iconHref = icon.getAttribute('href');
+ this.state.set('apps', apps);
+ } catch (error) {
+ handleAPIError(error);
+ } finally {
+ this.state.set('isLoading', false);
+ }
+ })();
- button.classList.add('loading');
- button.disabled = true;
- icon.setAttribute('href', '#icon-loading');
+ this.startAppWorking = (appId) => {
+ const apps = this.state.get('apps');
+ const app = apps.find(({ id }) => id === appId);
+ app.working = true;
+ this.state.set('apps', apps);
+ };
- return () => {
- button.classList.remove('loading');
- button.disabled = false;
- icon.setAttribute('href', iconHref);
+ this.stopAppWorking = (appId) => {
+ const apps = this.state.get('apps');
+ const app = apps.find(({ id }) => id === appId);
+ delete app.working;
+ this.state.set('apps', apps);
};
-};
-
-const promptSubscription = async ({ latest, purchaseType = 'buy' }, instance) => {
- let data = null;
- try {
- data = await APIClient.get(`apps?buildExternalUrl=true&appId=${ latest.id }&purchaseType=${ purchaseType }`);
- } catch (e) {
- handleAPIError(e, instance);
- return;
- }
-
- modal.open({
- allowOutsideClick: false,
- data,
- template: 'iframeModal',
- }, async () => {
- try {
- await APIClient.post('apps/', {
- appId: latest.id,
- marketplace: true,
- version: latest.version,
- });
- await Promise.all([
- getInstalledApps(instance),
- getApps(instance),
- ]);
- } catch (e) {
- handleAPIError(e, instance);
- }
- });
-};
-
-const setAppStatus = async (installedApp, status, instance) => {
- try {
- const result = await APIClient.post(`apps/${ installedApp.latest.id }/status`, { status });
- installedApp.latest.status = result.status;
- instance.installedApps.set(instance.installedApps.get());
- } catch (e) {
- handleAPIError(e, instance);
- }
-};
-
-const activateApp = (installedApp, instance) => {
- if (!isLoggedInCloud(instance)) {
- return;
- }
-
- setAppStatus(installedApp, 'manually_enabled', instance);
-};
-
-const promptAppDeactivation = (installedApp, instance) => {
- if (!isLoggedInCloud(instance)) {
- return;
- }
-
- modal.open({
- text: t('Apps_Marketplace_Deactivate_App_Prompt'),
- type: 'warning',
- showCancelButton: true,
- confirmButtonColor: '#DD6B55',
- confirmButtonText: t('Yes'),
- cancelButtonText: t('No'),
- closeOnConfirm: true,
- html: false,
- }, (confirmed) => {
- if (!confirmed) {
- return;
- }
- setAppStatus(installedApp, 'manually_disabled', instance);
- });
-};
-
-const uninstallApp = async (installedApp, instance) => {
- try {
- await APIClient.delete(`apps/${ installedApp.latest.id }`);
- const installedApps = instance.installedApps.get().filter((app) => app.latest.id !== installedApp.latest.id);
- instance.installedApps.set(installedApps);
- } catch (e) {
- handleAPIError(e, instance);
- }
-};
-
-const promptAppUninstall = (installedApp, instance) => {
- if (!isLoggedInCloud(instance)) {
- return;
- }
-
- modal.open({
- text: t('Apps_Marketplace_Uninstall_App_Prompt'),
- type: 'warning',
- showCancelButton: true,
- confirmButtonColor: '#DD6B55',
- confirmButtonText: t('Yes'),
- cancelButtonText: t('No'),
- closeOnConfirm: true,
- html: false,
- }, (confirmed) => {
- if (!confirmed) {
- return;
- }
- uninstallApp(installedApp, instance);
- });
-};
-Template.marketplace.onCreated(function() {
- this.ready = new ReactiveVar(false);
- this.apps = new ReactiveVar([]);
- this.installedApps = new ReactiveVar([]);
- this.searchText = new ReactiveVar('');
- this.searchSortBy = new ReactiveVar('name');
- this.sortDirection = new ReactiveVar('asc');
- this.limit = new ReactiveVar(0);
- this.page = new ReactiveVar(0);
- this.end = new ReactiveVar(false);
- this.isLoading = new ReactiveVar(true);
- this.cloudLoggedIn = new ReactiveVar(false);
-
- getInstalledApps(this);
- getApps(this);
- getCloudLoggedIn(this);
-
- this.onAppAdded = async (appId) => {
- const installedApps = this.installedApps.get().filter((installedApp) => installedApp.appId !== appId);
+ this.handleAppAddedOrUpdated = async (appId) => {
try {
- const { app } = await APIClient.get(`apps/${ appId }`);
- installedApps.push({ latest: app });
- this.installedApps.set(installedApps);
- } catch (e) {
- handleAPIError(e, this);
+ const { status, version } = await Apps.getApp(appId);
+ const app = await Apps.getAppFromMarketplace(appId, version);
+ const apps = [
+ ...this.state.get('apps').filter(({ id }) => id !== appId),
+ {
+ ...app,
+ installed: true,
+ status,
+ version,
+ marketplaceVersion: app.version,
+ },
+ ];
+ this.state.set('apps', apps);
+ } catch (error) {
+ handleAPIError(error);
}
};
- this.onAppRemoved = (appId) => {
- const apps = this.apps.get().filter(({ id }) => id !== appId);
- this.apps.set(apps);
+ this.handleAppRemoved = (appId) => {
+ const apps = this.state.get('apps').map((app) => {
+ if (app.id === appId) {
+ delete app.installed;
+ delete app.status;
+ app.version = app.marketplaceVersion;
+ }
+
+ return app;
+ });
+ this.state.set('apps', apps);
+ };
+
+ this.handleAppStatusChange = ({ appId, status }) => {
+ const apps = this.state.get('apps');
+ const app = apps.find(({ id }) => id === appId);
+ if (!app) {
+ return;
+ }
+
+ app.status = status;
+ this.state.set('apps', apps);
};
- Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.onAppAdded);
- Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.onAppRemoved);
+ Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated);
+ Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated);
+ Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleAppRemoved);
+ Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange);
});
Template.marketplace.onDestroyed(function() {
- Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.onAppAdded);
- Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.onAppRemoved);
+ Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated);
+ Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated);
+ Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleAppRemoved);
+ Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange);
});
-Template.marketplace.helpers({
- isReady() {
- if (Template.instance().ready != null) {
- return Template.instance().ready.get();
- }
-
- return false;
- },
- apps() {
- const instance = Template.instance();
- const searchText = instance.searchText.get().toLowerCase();
- const sortColumn = instance.searchSortBy.get();
- const inverted = instance.sortDirection.get() === 'desc';
- const apps = instance.apps.get().filter((app) => app.latest.name.toLowerCase().includes(searchText));
- return sortByColumn(apps, sortColumn, inverted);
- },
- appsDevelopmentMode() {
- return settings.get('Apps_Framework_Development_Mode') === true;
- },
- cloudLoggedIn() {
- return Template.instance().cloudLoggedIn.get();
- },
- parseStatus(status) {
- return t(`App_status_${ status }`);
- },
- isActive(status) {
- return enabled({ status });
- },
- sortIcon(key) {
- const {
- sortDirection,
- searchSortBy,
- } = Template.instance();
+Template.marketplace.onRendered(() => {
+ Tracker.afterFlush(() => {
+ SideNav.setFlex('adminFlex');
+ SideNav.openFlex();
+ });
+});
- return key === searchSortBy.get() && sortDirection.get() !== 'asc' ? 'sort-up' : 'sort-down';
- },
- searchSortBy(key) {
- return Template.instance().searchSortBy.get() === key;
+Template.marketplace.helpers({
+ isLoggedInCloud() {
+ return Template.instance().state.get('isLoggedInCloud');
},
isLoading() {
- return Template.instance().isLoading.get();
+ return Template.instance().state.get('isLoading');
},
- onTableScroll() {
- const instance = Template.instance();
- if (instance.loading || instance.end.get()) {
+ handleTableScroll() {
+ const { state } = Template.instance();
+ if (state.get('isLoading') || state.get('wasEndReached')) {
return;
}
- return function(currentTarget) {
- if (currentTarget.offsetHeight + currentTarget.scrollTop >= currentTarget.scrollHeight - 100) {
- return instance.page.set(instance.page.get() + 1);
+
+ return ({ offsetHeight, scrollTop, scrollHeight }) => {
+ const shouldGoToNextPage = offsetHeight + scrollTop >= scrollHeight - 100;
+ if (shouldGoToNextPage) {
+ return state.set('page', state.get('page') + 1);
}
};
},
- onTableResize() {
- const { limit } = Template.instance();
+ handleTableResize() {
+ const { state } = Template.instance();
return function() {
- limit.set(Math.ceil((this.$('.table-scroll').height() / 40) + 5));
+ const $table = this.$('.table-scroll');
+ state.set('itemsPerPage', Math.ceil(($table.height() / 40) + 5));
};
},
- onTableSort() {
- const { end, page, sortDirection, searchSortBy } = Template.instance();
- return function(type) {
- end.set(false);
- page.set(0);
-
- if (searchSortBy.get() === type) {
- sortDirection.set(sortDirection.get() === 'asc' ? 'desc' : 'asc');
+ handleTableSort() {
+ const { state } = Template.instance();
+
+ return (sortedColumn) => {
+ state.set({
+ page: 0,
+ wasEndReached: false,
+ });
+
+ if (state.get('sortedColumn') === sortedColumn) {
+ state.set('isAscendingOrder', !state.get('isAscendingOrder'));
return;
}
- searchSortBy.set(type);
- sortDirection.set('asc');
+ state.set({
+ sortedColumn,
+ isAscendingOrder: true,
+ });
};
},
- purchaseTypeDisplay(app) {
- if (app.purchaseType === 'subscription') {
+ isSortingBy(column) {
+ return Template.instance().state.get('sortedColumn') === column;
+ },
+ sortIcon(column) {
+ const { state } = Template.instance();
+
+ return column === state.get('sortedColumn') && state.get('isAscendingOrder') ? 'sort-down' : 'sort-up';
+ },
+ apps() {
+ const { state } = Template.instance();
+ const apps = state.get('apps');
+ const searchText = state.get('searchText').toLocaleLowerCase();
+ const sortedColumn = state.get('sortedColumn');
+ const isAscendingOrder = state.get('isAscendingOrder');
+ const sortingFactor = isAscendingOrder ? 1 : -1;
+
+ return apps
+ .filter(({ name }) => name.toLocaleLowerCase().includes(searchText))
+ .sort(({ [sortedColumn]: a }, { [sortedColumn]: b }) => sortingFactor * String(a).localeCompare(String(b)));
+ },
+ purchaseTypeDisplay({ purchaseType, price }) {
+ if (purchaseType === 'subscription') {
return t('Subscription');
}
- if (app.price > 0) {
+ if (price > 0) {
return t('Paid');
}
return t('Free');
},
- priceDisplay(app) {
- if (app.purchaseType === 'subscription') {
- if (!app.pricingPlans || !Array.isArray(app.pricingPlans) || app.pricingPlans.length === 0) {
+ priceDisplay({ purchaseType, pricingPlans, price }) {
+ if (purchaseType === 'subscription') {
+ if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) {
return '-';
}
- return formatPricingPlan(app.pricingPlans[0]);
+ return formatPricingPlan(pricingPlans[0]);
}
- if (app.price > 0) {
- return formatPrice(app.price);
+ if (price > 0) {
+ return formatPrice(price);
}
return '-';
},
- isInstalled(app) {
- const { installedApps } = Template.instance();
- const installedApp = installedApps.get().find(({ latest: { id } }) => id === app.latest.id);
- return !!installedApp;
- },
- isOnTrialPeriod(app) {
- return app.subscriptionInfo.status === 'trialing';
- },
- canUpdate(app) {
- const { installedApps } = Template.instance();
- const installedApp = installedApps.get().find(({ latest: { id } }) => id === app.latest.id);
- return !!installedApp && semver.lt(installedApp.latest.version, app.latest.version);
- },
- canTrial(app) {
- return app.purchaseType === 'subscription' && !app.subscriptionInfo.status;
- },
- canBuy(app) {
- return app.price > 0;
- },
+ appButtonProps,
+ appStatusSpanProps,
});
Template.marketplace.events({
- 'click [data-button="install"]'() {
- FlowRouter.go('/admin/app/install');
+ 'click .js-cloud-login'() {
+ FlowRouter.go('cloud-config');
},
- 'click [data-button="login"]'() {
- FlowRouter.go('/admin/cloud');
+ 'submit .js-search-form'(event) {
+ event.stopPropagation();
+ return false;
+ },
+ 'keyup .js-search'(event, instance) {
+ instance.state.set('searchText', event.currentTarget.value);
},
- 'click .js-open'(e) {
- e.stopPropagation();
- const { latest: { id, version } } = this;
- FlowRouter.go(`/admin/apps/${ id }?version=${ version }`);
+ 'click .js-open'(event, instance) {
+ event.stopPropagation();
+ const { currentTarget } = event;
+ const {
+ id: appId,
+ version,
+ marketplaceVersion,
+ } = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id);
+ FlowRouter.go('marketplace-app', { appId }, { version: version || marketplaceVersion });
},
- async 'click .js-install'(e, instance) {
- e.stopPropagation();
+ async 'click .js-install, click .js-update'(event, instance) {
+ event.preventDefault();
+ event.stopPropagation();
- if (!isLoggedInCloud(instance)) {
+ const isLoggedInCloud = await checkCloudLogin();
+ instance.state.set('isLoggedInCloud', isLoggedInCloud);
+ if (!isLoggedInCloud) {
return;
}
- const { currentTarget: button } = e;
- const stopLoading = triggerButtonLoadingState(button);
+ const { currentTarget: button } = event;
+ const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id);
- const { latest } = this;
+ instance.startAppWorking(app.id);
try {
- await APIClient.post('apps/', {
- appId: latest.id,
- marketplace: true,
- version: latest.version,
- });
- await Promise.all([
- getInstalledApps(instance),
- getApps(instance),
- ]);
- } catch (e) {
- handleAPIError(e, instance);
+ const { status } = await Apps.installApp(app.id, app.marketplaceVersion);
+ warnStatusChange(app.name, status);
+ } catch (error) {
+ handleAPIError(error);
} finally {
- stopLoading();
+ instance.stopAppWorking(app.id);
}
},
- async 'click .js-purchase'(e, instance) {
- e.stopPropagation();
+ async 'click .js-purchase'(event, instance) {
+ event.preventDefault();
+ event.stopPropagation();
- if (!isLoggedInCloud(instance)) {
+ const isLoggedInCloud = await checkCloudLogin();
+ instance.state.set('isLoggedInCloud', isLoggedInCloud);
+ if (!isLoggedInCloud) {
return;
}
- const { latest, purchaseType = 'buy' } = this;
- const { currentTarget: button } = e;
- const stopLoading = triggerButtonLoadingState(button);
+ const { currentTarget: button } = event;
+ const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id);
- let data = null;
- try {
- data = await APIClient.get(`apps?buildExternalUrl=true&appId=${ latest.id }&purchaseType=${ purchaseType }`);
- } catch (e) {
- handleAPIError(e, instance);
- stopLoading();
- return;
- }
+ instance.startAppWorking(app.id);
- modal.open({
- allowOutsideClick: false,
- data,
- template: 'iframeModal',
- }, async () => {
+ await promptSubscription(app, async () => {
try {
- await APIClient.post('apps/', {
- appId: latest.id,
- marketplace: true,
- version: latest.version,
- });
- await Promise.all([
- getInstalledApps(instance),
- getApps(instance),
- ]);
- } catch (e) {
- handleAPIError(e, instance);
+ const { status } = await Apps.installApp(app.id, app.marketplaceVersion);
+ warnStatusChange(app.name, status);
+ } catch (error) {
+ handleAPIError(error);
} finally {
- stopLoading();
+ instance.stopAppWorking(app.id);
}
- }, stopLoading);
+ }, instance.stopAppWorking.bind(instance, app.id));
},
- 'click .js-menu'(e, instance) {
- e.stopPropagation();
- const { currentTarget } = e;
-
- const installedApp = instance.installedApps.get().find(({ latest: { id } }) => id === this.latest.id);
- const isActive = installedApp && ['auto_enabled', 'manually_enabled'].includes(installedApp.latest.status);
-
- popover.open({
- currentTarget,
- instance,
- columns: [{
- groups: [
- ...this.purchaseType === 'subscription' ? [{
- items: [
- {
- icon: 'card',
- name: t('Subscription'),
- action: () => promptSubscription(this, instance),
- },
- ],
- }] : [],
- {
- items: [
- isActive
- ? {
- icon: 'ban',
- name: t('Deactivate'),
- modifier: 'alert',
- action: () => promptAppDeactivation(installedApp, instance),
- }
- : {
- icon: 'check',
- name: t('Activate'),
- action: () => activateApp(installedApp, instance),
- },
- {
- icon: 'trash',
- name: t('Uninstall'),
- modifier: 'alert',
- action: () => promptAppUninstall(installedApp, instance),
- },
- ],
- },
- ],
- }],
- });
- },
- 'keyup .js-search'(e, t) {
- t.searchText.set(e.currentTarget.value);
- },
- 'submit .js-search-form'(e) {
- e.preventDefault();
- e.stopPropagation();
- },
-});
+ 'click .js-menu'(event, instance) {
+ event.stopPropagation();
+ const { currentTarget } = event;
-Template.marketplace.onRendered(() => {
- Tracker.afterFlush(() => {
- SideNav.setFlex('adminFlex');
- SideNav.openFlex();
- });
+ const app = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id);
+ triggerAppPopoverMenu(app, currentTarget, instance);
+ },
});
diff --git a/app/apps/client/communication/index.js b/app/apps/client/communication/index.js
index 8878b65fcf21..321bbb7f15b7 100644
--- a/app/apps/client/communication/index.js
+++ b/app/apps/client/communication/index.js
@@ -1,3 +1 @@
-import { AppWebsocketReceiver, AppEvents } from './websockets';
-
-export { AppWebsocketReceiver, AppEvents };
+export { AppWebsocketReceiver, AppEvents } from './websockets';
diff --git a/app/apps/client/communication/websockets.js b/app/apps/client/communication/websockets.js
index 1da0efecccf2..aa1a23d451cb 100644
--- a/app/apps/client/communication/websockets.js
+++ b/app/apps/client/communication/websockets.js
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
+import EventEmitter from 'wolfy87-eventemitter';
import { slashCommands, APIClient } from '../../../utils';
import { CachedCollectionManager } from '../../../ui-cached-collection';
@@ -15,79 +16,43 @@ export const AppEvents = Object.freeze({
COMMAND_REMOVED: 'command/removed',
});
-export class AppWebsocketReceiver {
- constructor(orch) {
- this.orch = orch;
+export class AppWebsocketReceiver extends EventEmitter {
+ constructor() {
+ super();
+
this.streamer = new Meteor.Streamer('apps');
CachedCollectionManager.onLogin(() => {
this.listenStreamerEvents();
});
-
- this.listeners = {};
-
- Object.keys(AppEvents).forEach((v) => {
- this.listeners[AppEvents[v]] = [];
- });
}
listenStreamerEvents() {
- this.streamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this));
- this.streamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this));
- this.streamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this));
- this.streamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this));
- this.streamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this));
- this.streamer.on(AppEvents.COMMAND_ADDED, this.onCommandAdded.bind(this));
- this.streamer.on(AppEvents.COMMAND_DISABLED, this.onCommandDisabled.bind(this));
- this.streamer.on(AppEvents.COMMAND_UPDATED, this.onCommandUpdated.bind(this));
- this.streamer.on(AppEvents.COMMAND_REMOVED, this.onCommandDisabled.bind(this));
- }
-
- registerListener(event, listener) {
- this.listeners[event].push(listener);
- }
-
- unregisterListener(event, listener) {
- this.listeners[event].splice(this.listeners[event].indexOf(listener), 1);
- }
-
- onAppAdded(appId) {
- APIClient.get(`apps/${ appId }/languages`).then((result) => {
- this.orch.parseAndLoadLanguages(result.languages, appId);
+ Object.values(AppEvents).forEach((eventName) => {
+ this.streamer.on(eventName, this.emit.bind(this, eventName));
});
- this.listeners[AppEvents.APP_ADDED].forEach((listener) => listener(appId));
+ this.streamer.on(AppEvents.COMMAND_ADDED, this.onCommandAddedOrUpdated);
+ this.streamer.on(AppEvents.COMMAND_UPDATED, this.onCommandAddedOrUpdated);
+ this.streamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemovedOrDisabled);
+ this.streamer.on(AppEvents.COMMAND_DISABLED, this.onCommandRemovedOrDisabled);
}
- onAppRemoved(appId) {
- this.listeners[AppEvents.APP_REMOVED].forEach((listener) => listener(appId));
- }
-
- onAppUpdated(appId) {
- this.listeners[AppEvents.APP_UPDATED].forEach((listener) => listener(appId));
- }
-
- onAppStatusUpdated({ appId, status }) {
- this.listeners[AppEvents.APP_STATUS_CHANGE].forEach((listener) => listener({ appId, status }));
+ registerListener(event, listener) {
+ this.on(event, listener);
}
- onAppSettingUpdated({ appId }) {
- this.listeners[AppEvents.APP_SETTING_UPDATED].forEach((listener) => listener({ appId }));
+ unregisterListener(event, listener) {
+ this.off(event, listener);
}
- onCommandAdded(command) {
+ onCommandAddedOrUpdated = (command) => {
APIClient.v1.get('commands.get', { command }).then((result) => {
slashCommands.commands[command] = result.command;
});
}
- onCommandDisabled(command) {
+ onCommandRemovedOrDisabled = (command) => {
delete slashCommands.commands[command];
}
-
- onCommandUpdated(command) {
- APIClient.v1.get('commands.get', { command }).then((result) => {
- slashCommands.commands[command] = result.command;
- });
- }
}
diff --git a/app/apps/client/i18n.js b/app/apps/client/i18n.js
new file mode 100644
index 000000000000..9b9b1a9755e2
--- /dev/null
+++ b/app/apps/client/i18n.js
@@ -0,0 +1,38 @@
+import { TAPi18next } from 'meteor/rocketchat:tap-i18n';
+
+import { Apps } from './orchestrator';
+import { Utilities } from '../lib/misc/Utilities';
+import { AppEvents } from './communication';
+
+
+export const loadAppI18nResources = (appId, languages) => {
+ Object.entries(languages).forEach(([language, translations]) => {
+ try {
+ // Translations keys must be scoped under app id
+ const scopedTranslations = Object.entries(translations)
+ .reduce((translations, [key, value]) => {
+ translations[Utilities.getI18nKeyForApp(key, appId)] = value;
+ return translations;
+ }, {});
+
+ TAPi18next.addResourceBundle(language, 'project', scopedTranslations);
+ } catch (error) {
+ Apps.handleError(error);
+ }
+ });
+};
+
+const handleAppAdded = async (appId) => {
+ const languages = await Apps.getAppLanguages(appId);
+ loadAppI18nResources(appId, languages);
+};
+
+export const handleI18nResources = async () => {
+ const apps = await Apps.getAppsLanguages();
+ apps.forEach(({ id, languages }) => {
+ loadAppI18nResources(id, languages);
+ });
+
+ Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, handleAppAdded);
+ Apps.getWsListener().registerListener(AppEvents.APP_ADDED, handleAppAdded);
+};
diff --git a/app/apps/client/index.js b/app/apps/client/index.js
index 652c46ff95ee..887964154a90 100644
--- a/app/apps/client/index.js
+++ b/app/apps/client/index.js
@@ -1,7 +1,6 @@
import './admin/modalTemplates/iframeModal.html';
import './admin/modalTemplates/iframeModal';
import './admin/marketplace';
-import './admin/apps.html';
import './admin/apps';
import './admin/appInstall.html';
import './admin/appInstall';
@@ -10,5 +9,6 @@ import './admin/appLogs';
import './admin/appManage';
import './admin/appWhatIsIt.html';
import './admin/appWhatIsIt';
+import './routes';
export { Apps } from './orchestrator';
diff --git a/app/apps/client/orchestrator.js b/app/apps/client/orchestrator.js
index fb4de6700997..28b06e558de4 100644
--- a/app/apps/client/orchestrator.js
+++ b/app/apps/client/orchestrator.js
@@ -1,189 +1,188 @@
import { Meteor } from 'meteor/meteor';
-import { FlowRouter } from 'meteor/kadira:flow-router';
-import { BlazeLayout } from 'meteor/kadira:blaze-layout';
-import { TAPi18next } from 'meteor/tap:i18n';
+import toastr from 'toastr';
import { AppWebsocketReceiver } from './communication';
-import { Utilities } from '../lib/misc/Utilities';
import { APIClient } from '../../utils';
import { AdminBox } from '../../ui-utils';
import { CachedCollectionManager } from '../../ui-cached-collection';
import { hasAtLeastOnePermission } from '../../authorization';
+import { handleI18nResources } from './i18n';
+
+const createDeferredValue = () => {
+ let resolve;
+ let reject;
+ const promise = new Promise((_resolve, _reject) => {
+ resolve = _resolve;
+ reject = _reject;
+ });
-export let Apps;
+ return [promise, resolve, reject];
+};
class AppClientOrchestrator {
constructor() {
- this._isLoaded = false;
- this._isEnabled = false;
- this._loadingResolve;
- this._refreshLoading();
- }
-
- isLoaded() {
- return this._isLoaded;
+ this.isLoaded = false;
+ [this.deferredIsEnabled, this.setEnabled] = createDeferredValue();
}
- isEnabled() {
- return this._isEnabled;
- }
-
- getLoadingPromise() {
- if (this._isLoaded) {
- return Promise.resolve(this._isEnabled);
+ load = async (isEnabled) => {
+ if (!this.isLoaded) {
+ this.ws = new AppWebsocketReceiver();
+ this.registerAdminMenuItems();
+ this.isLoaded = true;
}
- return this._loadingPromise;
- }
-
- load(isEnabled) {
- this._isEnabled = isEnabled;
+ this.setEnabled(isEnabled);
- // It was already loaded, so let's load it again
- if (this._isLoaded) {
- this._refreshLoading();
- } else {
- this.ws = new AppWebsocketReceiver(this);
- this._addAdminMenuOption();
- }
-
- Meteor.defer(() => {
- this._loadLanguages().then(() => {
- this._loadingResolve(this._isEnabled);
- this._isLoaded = true;
- });
- });
- }
+ // Since the deferred value (a promise) is immutable after resolved,
+ // it need to be recreated to resolve a new value
+ [this.deferredIsEnabled, this.setEnabled] = createDeferredValue();
- getWsListener() {
- return this.ws;
+ await handleI18nResources();
+ this.setEnabled(isEnabled);
}
- _refreshLoading() {
- this._loadingPromise = new Promise((resolve) => {
- this._loadingResolve = resolve;
- });
- }
+ getWsListener = () => this.ws
- _addAdminMenuOption() {
+ registerAdminMenuItems = () => {
AdminBox.addOption({
icon: 'cube',
href: 'apps',
i18nLabel: 'Apps',
- permissionGranted() {
- return hasAtLeastOnePermission(['manage-apps']);
- },
+ permissionGranted: () => hasAtLeastOnePermission(['manage-apps']),
});
AdminBox.addOption({
icon: 'cube',
href: 'marketplace',
i18nLabel: 'Marketplace',
- permissionGranted() {
- return hasAtLeastOnePermission(['manage-apps']);
- },
+ permissionGranted: () => hasAtLeastOnePermission(['manage-apps']),
});
}
- _loadLanguages() {
- return APIClient.get('apps/languages').then((info) => {
- info.apps.forEach((rlInfo) => this.parseAndLoadLanguages(rlInfo.languages, rlInfo.id));
- });
+ handleError = (error) => {
+ console.error(error);
+ if (hasAtLeastOnePermission(['manage-apps'])) {
+ toastr.error(error.message);
+ }
}
- parseAndLoadLanguages(languages, id) {
- Object.entries(languages).forEach(([language, translations]) => {
- try {
- translations = Object.entries(translations).reduce((newTranslations, [key, value]) => {
- newTranslations[Utilities.getI18nKeyForApp(key, id)] = value;
- return newTranslations;
- }, {});
+ isEnabled = () => this.deferredIsEnabled
- TAPi18next.addResourceBundle(language, 'project', translations);
- } catch (e) {
- // Failed to parse the json
- }
+ getApps = async () => {
+ const { apps } = await APIClient.get('apps');
+ return apps;
+ }
+
+ getAppsFromMarketplace = async () => {
+ const appsOverviews = await APIClient.get('apps', { marketplace: 'true' });
+ return appsOverviews.map(({ latest, price, pricingPlans, purchaseType }) => ({
+ ...latest,
+ price,
+ pricingPlans,
+ purchaseType,
+ }));
+ }
+
+ getAppsOnBundle = async (bundleId) => {
+ const { apps } = await APIClient.get(`apps/bundles/${ bundleId }/apps`);
+ return apps;
+ }
+
+ getAppsLanguages = async () => {
+ const { apps } = await APIClient.get('apps/languages');
+ return apps;
+ }
+
+ getApp = async (appId) => {
+ const { app } = await APIClient.get(`apps/${ appId }`);
+ return app;
+ }
+
+ getAppFromMarketplace = async (appId, version) => {
+ const { app } = await APIClient.get(`apps/${ appId }`, {
+ marketplace: 'true',
+ version,
});
+ return app;
}
- async getAppApis(appId) {
- const result = await APIClient.get(`apps/${ appId }/apis`);
- return result.apis;
+ getLatestAppFromMarketplace = async (appId, version) => {
+ const { app } = await APIClient.get(`apps/${ appId }`, {
+ marketplace: 'true',
+ update: 'true',
+ appVersion: version,
+ });
+ return app;
+ }
+
+ getAppSettings = async (appId) => {
+ const { settings } = await APIClient.get(`apps/${ appId }/settings`);
+ return settings;
}
-}
-Meteor.startup(function _rlClientOrch() {
- Apps = new AppClientOrchestrator();
+ setAppSettings = async (appId, settings) => {
+ const { updated } = await APIClient.post(`apps/${ appId }/settings`, undefined, { settings });
+ return updated;
+ }
- CachedCollectionManager.onLogin(() => {
- Meteor.call('apps/is-enabled', (error, isEnabled) => {
- Apps.load(isEnabled);
+ getAppApis = async (appId) => {
+ const { apis } = await APIClient.get(`apps/${ appId }/apis`);
+ return apis;
+ }
+
+ getAppLanguages = async (appId) => {
+ const { languages } = await APIClient.get(`apps/${ appId }/languages`);
+ return languages;
+ }
+
+ installApp = async (appId, version) => {
+ const { app } = await APIClient.post('apps/', {
+ appId,
+ marketplace: true,
+ version,
});
- });
-});
+ return app;
+ }
-const appsRouteAction = function _theRealAction(whichCenter) {
- Meteor.defer(() => Apps.getLoadingPromise().then((isEnabled) => {
- if (isEnabled) {
- BlazeLayout.render('main', { center: whichCenter, old: true }); // TODO remove old
- } else {
- FlowRouter.go('app-what-is-it');
- }
- }));
-};
+ uninstallApp = (appId) => APIClient.delete(`apps/${ appId }`)
-// Bah, this has to be done *before* `Meteor.startup`
-FlowRouter.route('/admin/marketplace', {
- name: 'marketplace',
- action() {
- appsRouteAction('marketplace');
- },
-});
+ syncApp = (appId) => APIClient.post(`apps/${ appId }/sync`)
-FlowRouter.route('/admin/marketplace/:itemId', {
- name: 'app-manage',
- action() {
- appsRouteAction('appManage');
- },
-});
+ setAppStatus = async (appId, status) => {
+ const { status: effectiveStatus } = await APIClient.post(`apps/${ appId }/status`, { status });
+ return effectiveStatus;
+ }
-FlowRouter.route('/admin/apps', {
- name: 'apps',
- action() {
- appsRouteAction('apps');
- },
-});
+ enableApp = (appId) => this.setAppStatus(appId, 'manually_enabled')
-FlowRouter.route('/admin/app/install', {
- name: 'app-install',
- action() {
- appsRouteAction('appInstall');
- },
-});
+ disableApp = (appId) => this.setAppStatus(appId, 'manually_disabled')
-FlowRouter.route('/admin/apps/:appId', {
- name: 'app-manage',
- action() {
- appsRouteAction('appManage');
- },
-});
+ buildExternalUrl = (appId, purchaseType = 'buy', details = false) =>
+ APIClient.get('apps', {
+ buildExternalUrl: 'true',
+ appId,
+ purchaseType,
+ details,
+ })
-FlowRouter.route('/admin/apps/:appId/logs', {
- name: 'app-logs',
- action() {
- appsRouteAction('appLogs');
- },
-});
+ getCategories = async () => {
+ const categories = await APIClient.get('apps', { categories: 'true' });
+ return categories;
+ }
+}
+
+export const Apps = new AppClientOrchestrator();
-FlowRouter.route('/admin/app/what-is-it', {
- name: 'app-what-is-it',
- action() {
- Meteor.defer(() => Apps.getLoadingPromise().then((isEnabled) => {
- if (isEnabled) {
- FlowRouter.go('apps');
- } else {
- BlazeLayout.render('main', { center: 'appWhatIsIt' });
+Meteor.startup(() => {
+ CachedCollectionManager.onLogin(() => {
+ Meteor.call('apps/is-enabled', (error, isEnabled) => {
+ if (error) {
+ Apps.handleError(error);
+ return;
}
- }));
- },
+
+ Apps.load(isEnabled);
+ });
+ });
});
diff --git a/app/apps/client/routes.js b/app/apps/client/routes.js
new file mode 100644
index 000000000000..1a44f4d74bff
--- /dev/null
+++ b/app/apps/client/routes.js
@@ -0,0 +1,55 @@
+import { FlowRouter } from 'meteor/kadira:flow-router';
+import { BlazeLayout } from 'meteor/kadira:blaze-layout';
+
+import { Apps } from './orchestrator';
+
+FlowRouter.route('/admin/apps/what-is-it', {
+ name: 'apps-what-is-it',
+ action: async () => {
+ // TODO: render loading indicator
+ if (await Apps.isEnabled()) {
+ FlowRouter.go('apps');
+ } else {
+ BlazeLayout.render('main', { center: 'appWhatIsIt' });
+ }
+ },
+});
+
+const createAppsRouteAction = (centerTemplate) => async () => {
+ // TODO: render loading indicator
+ if (await Apps.isEnabled()) {
+ BlazeLayout.render('main', { center: centerTemplate, old: true }); // TODO remove old
+ } else {
+ FlowRouter.go('apps-what-is-it');
+ }
+};
+
+FlowRouter.route('/admin/apps', {
+ name: 'apps',
+ action: createAppsRouteAction('apps'),
+});
+
+FlowRouter.route('/admin/apps/install', {
+ name: 'app-install',
+ action: createAppsRouteAction('appInstall'),
+});
+
+FlowRouter.route('/admin/apps/:appId', {
+ name: 'app-manage',
+ action: createAppsRouteAction('appManage'),
+});
+
+FlowRouter.route('/admin/apps/:appId/logs', {
+ name: 'app-logs',
+ action: createAppsRouteAction('appLogs'),
+});
+
+FlowRouter.route('/admin/marketplace', {
+ name: 'marketplace',
+ action: createAppsRouteAction('marketplace'),
+});
+
+FlowRouter.route('/admin/marketplace/:appId', {
+ name: 'marketplace-app',
+ action: createAppsRouteAction('appManage'),
+});
diff --git a/app/apps/server/bridges/users.js b/app/apps/server/bridges/users.js
index 1485f7b3100a..7329777f7026 100644
--- a/app/apps/server/bridges/users.js
+++ b/app/apps/server/bridges/users.js
@@ -18,6 +18,6 @@ export class AppUserBridge {
}
async getActiveUserCount() {
- return Users.findActive().count() - Users.findActiveRemote().count();
+ return Users.getActiveLocalUserCount();
}
}
diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js
index 03e2bd3b77b5..5b0a4f19d40b 100644
--- a/app/apps/server/communication/rest.js
+++ b/app/apps/server/communication/rest.js
@@ -6,6 +6,8 @@ import { API } from '../../../api/server';
import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server';
import { settings } from '../../../settings';
import { Info } from '../../../utils';
+import { Settings, Users } from '../../../models/server';
+import { Apps } from '../orchestrator';
const getDefaultHeaders = () => ({
'X-Apps-Engine-Version': Info.marketplaceApiVersion,
@@ -69,11 +71,18 @@ export class AppsRestApi {
headers.Authorization = `Bearer ${ token }`;
}
- const result = HTTP.get(`${ baseUrl }/v1/apps?version=${ Info.marketplaceApiVersion }`, {
- headers,
- });
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/apps`, {
+ headers,
+ });
+ } catch (e) {
+ orchestrator.getRocketChatLogger().error('Error getting the Apps:', e.response.data);
+ return API.v1.internalError();
+ }
- if (result.statusCode !== 200) {
+ if (!result || result.statusCode !== 200) {
+ orchestrator.getRocketChatLogger().error('Error getting the Apps:', result.data);
return API.v1.failure();
}
@@ -87,11 +96,18 @@ export class AppsRestApi {
headers.Authorization = `Bearer ${ token }`;
}
- const result = HTTP.get(`${ baseUrl }/v1/categories`, {
- headers,
- });
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/categories`, {
+ headers,
+ });
+ } catch (e) {
+ orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', e.response.data);
+ return API.v1.internalError();
+ }
- if (result.statusCode !== 200) {
+ if (!result || result.statusCode !== 200) {
+ orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', result.data);
return API.v1.failure();
}
@@ -110,10 +126,14 @@ export class AppsRestApi {
return API.v1.failure({ error: 'Unauthorized' });
}
+ const subscribeRoute = this.queryParams.details === 'true' ? 'subscribe/details' : 'subscribe';
+
+ const seats = Users.getActiveLocalUserCount();
+
return API.v1.success({
url: `${ baseUrl }/apps/${ this.queryParams.appId }/${
- this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : 'subscribe'
- }?workspaceId=${ workspaceId }&token=${ token }`,
+ this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : subscribeRoute
+ }?workspaceId=${ workspaceId }&token=${ token }&seats=${ seats }`,
});
}
@@ -136,7 +156,13 @@ export class AppsRestApi {
return API.v1.failure({ error: 'Installation from url is disabled.' });
}
- const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: null } });
+ let result;
+ try {
+ result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: null } });
+ } catch (e) {
+ orchestrator.getRocketChatLogger().error('Error getting the app from url:', e.response.data);
+ return API.v1.internalError();
+ }
if (result.statusCode !== 200 || !result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
@@ -221,7 +247,7 @@ export class AppsRestApi {
return API.v1.success({
app: info,
implemented: aff.getImplementedInferfaces(),
- warnings: aff.getLicenseValidationResult().getWarnings(),
+ licenseValidation: aff.getLicenseValidationResult(),
});
},
});
@@ -247,11 +273,18 @@ export class AppsRestApi {
headers.Authorization = `Bearer ${ token }`;
}
- const result = HTTP.get(`${ baseUrl }/v1/bundles/${ this.urlParams.id }/apps`, {
- headers,
- });
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/bundles/${ this.urlParams.id }/apps`, {
+ headers,
+ });
+ } catch (e) {
+ orchestrator.getRocketChatLogger().error('Error getting the Bundle\'s Apps from the Marketplace:', e.response.data);
+ return API.v1.internalError();
+ }
- if (result.statusCode !== 200 || result.data.length === 0) {
+ if (!result || result.statusCode !== 200 || result.data.length === 0) {
+ orchestrator.getRocketChatLogger().error('Error getting the Bundle\'s Apps from the Marketplace:', result.data);
return API.v1.failure();
}
@@ -259,22 +292,42 @@ export class AppsRestApi {
},
});
+ const handleError = (message, e) => {
+ orchestrator.getRocketChatLogger().error(message, e.response.data);
+
+ if (e.response.statusCode >= 500 && e.response.statusCode <= 599) {
+ return API.v1.internalError();
+ }
+
+ if (e.response.statusCode === 404) {
+ return API.v1.notFound();
+ }
+
+ return API.v1.failure();
+ };
+
this.api.addRoute(':id', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
if (this.queryParams.marketplace && this.queryParams.version) {
const baseUrl = orchestrator.getMarketplaceUrl();
- const headers = getDefaultHeaders();
+ const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE.
const token = getWorkspaceAccessToken();
if (token) {
headers.Authorization = `Bearer ${ token }`;
}
- const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }?appVersion=${ this.queryParams.version }`, {
- headers,
- });
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }?appVersion=${ this.queryParams.version }`, {
+ headers,
+ });
+ } catch (e) {
+ return handleError('Error getting the App information from the Marketplace:', e);
+ }
- if (result.statusCode !== 200 || result.data.length === 0) {
+ if (!result || result.statusCode !== 200 || result.data.length === 0) {
+ orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', result.data);
return API.v1.failure();
}
@@ -290,11 +343,17 @@ export class AppsRestApi {
headers.Authorization = `Bearer ${ token }`;
}
- const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }/latest?frameworkVersion=${ Info.marketplaceApiVersion }`, {
- headers,
- });
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }/latest?frameworkVersion=${ Info.marketplaceApiVersion }`, {
+ headers,
+ });
+ } catch (e) {
+ return handleError('Error getting the App update info from the Marketplace:', e);
+ }
if (result.statusCode !== 200 || result.data.length === 0) {
+ orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', result.data);
return API.v1.failure();
}
@@ -305,10 +364,16 @@ export class AppsRestApi {
if (prl) {
const info = prl.getInfo();
- info.status = prl.getStatus();
- return API.v1.success({ app: info });
+ return API.v1.success({
+ app: {
+ ...info,
+ status: prl.getStatus(),
+ licenseValidation: prl.getLatestLicenseValidationResult(),
+ },
+ });
}
+
return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`);
},
post() {
@@ -319,13 +384,13 @@ export class AppsRestApi {
return API.v1.failure({ error: 'Updating an App from a url is disabled.' });
}
- const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: 'binary' } });
+ const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: null } });
if (result.statusCode !== 200 || !result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
}
- buff = Buffer.from(result.content, 'binary');
+ buff = result.content;
} else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) {
const baseUrl = orchestrator.getMarketplaceUrl();
@@ -335,12 +400,19 @@ export class AppsRestApi {
headers.Authorization = `Bearer ${ token }`;
}
- const result = HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }`, {
- headers,
- npmRequestOptions: { encoding: 'binary' },
- });
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }`, {
+ headers,
+ npmRequestOptions: { encoding: null },
+ });
+ } catch (e) {
+ orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', e.response.data);
+ return API.v1.internalError();
+ }
if (result.statusCode !== 200) {
+ orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', result.data);
return API.v1.failure();
}
@@ -348,7 +420,7 @@ export class AppsRestApi {
return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' });
}
- buff = Buffer.from(result.content, 'binary');
+ buff = result.content;
} else {
if (settings.get('Apps_Framework_Development_Mode') !== true) {
return API.v1.failure({ error: 'Direct updating of an App is disabled.' });
@@ -392,6 +464,39 @@ export class AppsRestApi {
},
});
+ this.api.addRoute(':id/sync', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
+ post() {
+ const baseUrl = orchestrator.getMarketplaceUrl();
+
+ const headers = getDefaultHeaders();
+ const token = getWorkspaceAccessToken();
+ if (token) {
+ headers.Authorization = `Bearer ${ token }`;
+ }
+
+ const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch();
+
+ let result;
+ try {
+ result = HTTP.get(`${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps/${ this.urlParams.id }`, {
+ headers,
+ });
+ } catch (e) {
+ orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', e.response.data);
+ return API.v1.internalError();
+ }
+
+ if (result.statusCode !== 200) {
+ orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', result.data);
+ return API.v1.failure();
+ }
+
+ Promise.await(Apps.updateAppsMarketplaceInfo([result.data]));
+
+ return API.v1.success({ app: result.data });
+ },
+ });
+
this.api.addRoute(':id/icon', { authRequired: true, permissionsRequired: ['manage-apps'] }, {
get() {
const prl = manager.getOneById(this.urlParams.id);
diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js
index 94b9c22765a7..857158ddd3bb 100644
--- a/app/apps/server/communication/websockets.js
+++ b/app/apps/server/communication/websockets.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus';
+import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus';
export const AppEvents = Object.freeze({
APP_ADDED: 'app/added',
@@ -20,11 +20,12 @@ export class AppServerListener {
this.clientStreamer = clientStreamer;
this.received = received;
- this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this));
this.engineStreamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this));
- this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this));
this.engineStreamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this));
this.engineStreamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this));
+ this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this));
+
+ this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this));
this.engineStreamer.on(AppEvents.COMMAND_ADDED, this.onCommandAdded.bind(this));
this.engineStreamer.on(AppEvents.COMMAND_DISABLED, this.onCommandDisabled.bind(this));
this.engineStreamer.on(AppEvents.COMMAND_UPDATED, this.onCommandUpdated.bind(this));
@@ -36,21 +37,27 @@ export class AppServerListener {
this.clientStreamer.emit(AppEvents.APP_ADDED, appId);
}
+
async onAppStatusUpdated({ appId, status }) {
+ const app = this.orch.getManager().getOneById(appId);
+
+ if (app.getStatus() === status) {
+ return;
+ }
+
this.received.set(`${ AppEvents.APP_STATUS_CHANGE }_${ appId }`, { appId, status, when: new Date() });
if (AppStatusUtils.isEnabled(status)) {
- await this.orch.getManager().enable(appId);
+ await this.orch.getManager().enable(appId).catch(console.error);
this.clientStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status });
} else if (AppStatusUtils.isDisabled(status)) {
- await this.orch.getManager().disable(appId, AppStatus.MANUALLY_DISABLED === status);
+ await this.orch.getManager().disable(appId, status, true).catch(console.error);
this.clientStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status });
}
}
async onAppSettingUpdated({ appId, setting }) {
this.received.set(`${ AppEvents.APP_SETTING_UPDATED }_${ appId }_${ setting.id }`, { appId, setting, when: new Date() });
-
await this.orch.getManager().getSettingsManager().updateAppSetting(appId, setting);
this.clientStreamer.emit(AppEvents.APP_SETTING_UPDATED, { appId });
}
@@ -65,6 +72,12 @@ export class AppServerListener {
}
async onAppRemoved(appId) {
+ const app = this.orch.getManager().getOneById(appId);
+
+ if (!app) {
+ return;
+ }
+
await this.orch.getManager().remove(appId);
this.clientStreamer.emit(AppEvents.APP_REMOVED, appId);
}
diff --git a/app/apps/server/cron.js b/app/apps/server/cron.js
index df8bea673224..0dbed66a9bd3 100644
--- a/app/apps/server/cron.js
+++ b/app/apps/server/cron.js
@@ -1,17 +1,85 @@
import { Meteor } from 'meteor/meteor';
import { HTTP } from 'meteor/http';
import { SyncedCron } from 'meteor/littledata:synced-cron';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import { Apps } from './orchestrator';
import { getWorkspaceAccessToken } from '../../cloud/server';
-import { Settings } from '../../models/server';
+import { Settings, Users, Roles } from '../../models/server';
-export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(() => {
+
+const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdminsAboutInvalidApps(apps) {
+ const hasInvalidApps = !!apps.find((app) => app.getLatestLicenseValidationResult().hasErrors);
+
+ if (!hasInvalidApps) {
+ return apps;
+ }
+
+ const id = 'someAppInInvalidState';
+ const title = 'Warning';
+ const text = 'There is one or more apps in an invalid state. Click here to review.';
+ const rocketCatMessage = 'There is one or more apps in an invalid state. Go to Administration > Apps to review.';
+ const link = '/admin/apps';
+
+ Roles.findUsersInRole('admin').forEach((adminUser) => {
+ Users.removeBannerById(adminUser._id, { id });
+
+ try {
+ Meteor.runAsUser(adminUser._id, () => Meteor.call('createDirectMessage', 'rocket.cat'));
+
+ Meteor.runAsUser('rocket.cat', () => Meteor.call('sendMessage', {
+ msg: `*${ TAPi18n.__(title, adminUser.language) }*\n${ TAPi18n.__(rocketCatMessage, adminUser.language) }`,
+ rid: [adminUser._id, 'rocket.cat'].sort().join(''),
+ }));
+ } catch (e) {
+ console.error(e);
+ }
+
+ Users.addBannerById(adminUser._id, {
+ id,
+ priority: 10,
+ title,
+ text,
+ modifiers: ['danger'],
+ link,
+ });
+ });
+
+ return apps;
+});
+
+const notifyAdminsAboutRenewedApps = Meteor.bindEnvironment(function _notifyAdminsAboutRenewedApps(apps) {
+ const renewedApps = apps.filter((app) => app.getStatus() === AppStatus.DISABLED && app.getPreviousStatus() === AppStatus.INVALID_LICENSE_DISABLED);
+
+ if (renewedApps.length === 0) {
+ return;
+ }
+
+ const rocketCatMessage = 'There is one or more disabled apps with valid licenses. Go to Administration > Apps to review.';
+
+ Roles.findUsersInRole('admin').forEach((adminUser) => {
+ try {
+ Meteor.runAsUser(adminUser._id, () => Meteor.call('createDirectMessage', 'rocket.cat'));
+
+ Meteor.runAsUser('rocket.cat', () => Meteor.call('sendMessage', {
+ msg: `${ TAPi18n.__(rocketCatMessage, adminUser.language) }`,
+ rid: [adminUser._id, 'rocket.cat'].sort().join(''),
+ }));
+ } catch (e) {
+ console.error(e);
+ }
+ });
+});
+
+export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(function _appsUpdateMarketplaceInfo() {
const token = getWorkspaceAccessToken();
const baseUrl = Apps.getMarketplaceUrl();
const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch();
- const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps`;
+ const currentSeats = Users.getActiveLocalUserCount();
+
+ const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps?seats=${ currentSeats }`;
const options = {
headers: {
Authorization: `Bearer ${ token }`,
@@ -30,7 +98,11 @@ export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(() => {
Apps.debugLog(err);
}
- Promise.await(Apps.updateAppsMarketplaceInfo(data));
+ Promise.await(
+ Apps.updateAppsMarketplaceInfo(data)
+ .then(notifyAdminsAboutInvalidApps)
+ .then(notifyAdminsAboutRenewedApps)
+ );
});
SyncedCron.add({
diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js
index 6a4a2b3c92e8..427fae9f5006 100644
--- a/app/apps/server/orchestrator.js
+++ b/app/apps/server/orchestrator.js
@@ -7,11 +7,13 @@ import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsers
import { AppRealStorage, AppRealLogsStorage } from './storage';
import { settings } from '../../settings';
import { Permissions, AppsLogsModel, AppsModel, AppsPersistenceModel } from '../../models';
+import { Logger } from '../../logger';
export let Apps;
class AppServerOrchestrator {
constructor() {
+ this._rocketchatLogger = new Logger('Rocket.Chat Apps');
Permissions.createOrUpdate('manage-apps', ['admin']);
this._marketplaceUrl = 'https://marketplace.rocket.chat';
@@ -82,6 +84,10 @@ class AppServerOrchestrator {
return settings.get('Apps_Framework_Development_Mode');
}
+ getRocketChatLogger() {
+ return this._rocketchatLogger;
+ }
+
debugLog(...args) {
if (this.isDebugging()) {
// eslint-disable-next-line
@@ -93,11 +99,11 @@ class AppServerOrchestrator {
return this._marketplaceUrl;
}
- load() {
+ async load() {
// Don't try to load it again if it has
// already been loaded
if (this.isLoaded()) {
- return Promise.resolve();
+ return;
}
return this._manager.load()
@@ -105,11 +111,11 @@ class AppServerOrchestrator {
.catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err));
}
- unload() {
+ async unload() {
// Don't try to unload it if it's already been
// unlaoded or wasn't unloaded to start with
if (!this.isLoaded()) {
- return Promise.resolve();
+ return;
}
return this._manager.unload()
@@ -117,12 +123,13 @@ class AppServerOrchestrator {
.catch((err) => console.warn('Failed to unload the Apps Framework!', err));
}
- updateAppsMarketplaceInfo(apps = []) {
+ async updateAppsMarketplaceInfo(apps = []) {
if (!this.isLoaded()) {
- return Promise.resolve();
+ return;
}
- return this._manager.updateAppsMarketplaceInfo(apps);
+ return this._manager.updateAppsMarketplaceInfo(apps)
+ .then(() => this._manager.get());
}
}
diff --git a/app/apps/server/storage/storage.js b/app/apps/server/storage/storage.js
index 4b8c08eb572d..8fc9c79b705f 100644
--- a/app/apps/server/storage/storage.js
+++ b/app/apps/server/storage/storage.js
@@ -44,11 +44,7 @@ export class AppRealStorage extends AppStorage {
return reject(e);
}
- if (doc) {
- resolve(doc);
- } else {
- reject(new Error(`No App found by the id: ${ id }`));
- }
+ resolve(doc);
});
}
@@ -74,12 +70,11 @@ export class AppRealStorage extends AppStorage {
return new Promise((resolve, reject) => {
try {
this.db.update({ id: item.id }, item);
+ resolve(item.id);
} catch (e) {
return reject(e);
}
-
- this.retrieveOne(item.id).then((updated) => resolve(updated)).catch((err) => reject(err));
- });
+ }).then(this.retrieveOne.bind(this));
}
remove(id) {
diff --git a/app/authorization/server/index.js b/app/authorization/server/index.js
index 37a853a1f101..01ac6e9e8078 100644
--- a/app/authorization/server/index.js
+++ b/app/authorization/server/index.js
@@ -22,7 +22,6 @@ import './methods/removeUserFromRole';
import './methods/saveRole';
import './publications/permissions';
import './publications/roles';
-import './publications/usersInRole';
import './startup';
export {
diff --git a/app/authorization/server/publications/usersInRole.js b/app/authorization/server/publications/usersInRole.js
deleted file mode 100644
index f7eb22146e45..000000000000
--- a/app/authorization/server/publications/usersInRole.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { hasPermission } from '../functions/hasPermission';
-import { getUsersInRole } from '../functions/getUsersInRole';
-
-Meteor.publish('usersInRole', function(roleName, scope, limit = 50) {
- console.warn('The publication "usersInRole" is deprecated and will be removed after version v2.0.0');
-
- if (!this.userId) {
- return this.ready();
- }
-
- if (!hasPermission(this.userId, 'access-permissions')) {
- return this.error(new Meteor.Error('error-not-allowed', 'Not allowed', {
- publish: 'usersInRole',
- }));
- }
-
- const options = {
- limit,
- sort: {
- name: 1,
- },
- };
-
- return getUsersInRole(roleName, scope, options);
-});
diff --git a/app/autotranslate/client/lib/autotranslate.js b/app/autotranslate/client/lib/autotranslate.js
index 74d81891755f..bf3fbd7b64d6 100644
--- a/app/autotranslate/client/lib/autotranslate.js
+++ b/app/autotranslate/client/lib/autotranslate.js
@@ -64,6 +64,10 @@ export const AutoTranslate = {
this.supportedLanguages = languages || [];
});
+ Meteor.call('autoTranslate.getProviderUiMetadata', (err, metadata) => {
+ this.providersMetadata = metadata;
+ });
+
Tracker.autorun(() => {
Subscriptions.find().observeChanges({
changed: (id, fields) => {
diff --git a/app/autotranslate/client/stylesheets/autotranslate.css b/app/autotranslate/client/stylesheets/autotranslate.css
index 480502c4371e..c72c0555d759 100644
--- a/app/autotranslate/client/stylesheets/autotranslate.css
+++ b/app/autotranslate/client/stylesheets/autotranslate.css
@@ -58,6 +58,10 @@
left: 5px;
border-left: 0;
+
+ & .translation-provider {
+ display: none;
+ }
}
}
}
diff --git a/app/autotranslate/server/autotranslate.js b/app/autotranslate/server/autotranslate.js
index 13c77caf4c94..bae921b1024c 100644
--- a/app/autotranslate/server/autotranslate.js
+++ b/app/autotranslate/server/autotranslate.js
@@ -1,5 +1,4 @@
import { Meteor } from 'meteor/meteor';
-import { HTTP } from 'meteor/http';
import _ from 'underscore';
import s from 'underscore.string';
@@ -7,23 +6,83 @@ import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
import { Subscriptions, Messages } from '../../models';
import { Markdown } from '../../markdown/server';
+import { Logger } from '../../logger';
+
+/**
+ * This class allows translation providers to
+ * register,load and also returns the active provider.
+ */
+export class TranslationProviderRegistry {
+ /**
+ * Registers the translation provider into the registry.
+ * @param {*} provider
+ */
+ static registerProvider(provider) {
+ // get provider information
+ const metadata = provider._getProviderMetadata();
+ if (!TranslationProviderRegistry._providers) {
+ TranslationProviderRegistry._providers = {};
+ }
+ TranslationProviderRegistry._providers[metadata.name] = provider;
+ }
-class AutoTranslate {
+ /**
+ * Return the active Translation provider
+ */
+ static getActiveProvider() {
+ return TranslationProviderRegistry._providers[TranslationProviderRegistry._activeProvider];
+ }
+
+ /**
+ * Make the activated provider by setting as the active.
+ */
+ static loadActiveServiceProvider() {
+ settings.get('AutoTranslate_ServiceProvider', (key, value) => {
+ TranslationProviderRegistry._activeProvider = value;
+ });
+ }
+}
+
+/**
+ * Generic auto translate base implementation.
+ * This class provides generic parts of implementation for
+ * tokenization, detokenization, call back register and unregister.
+ * @abstract
+ * @class
+ */
+export class AutoTranslate {
+ /**
+ * Encapsulate the api key and provider settings.
+ * @constructor
+ */
constructor() {
+ this.name = '';
this.languages = [];
- this.enabled = settings.get('AutoTranslate_Enabled');
- this.apiKey = settings.get('AutoTranslate_GoogleAPIKey');
this.supportedLanguages = {};
- callbacks.add('afterSaveMessage', this.translateMessage.bind(this), callbacks.priority.MEDIUM, 'AutoTranslate');
+ // Get Auto Translate Active flag
settings.get('AutoTranslate_Enabled', (key, value) => {
- this.enabled = value;
+ this.autoTranslateEnabled = value;
});
- settings.get('AutoTranslate_GoogleAPIKey', (key, value) => {
- this.apiKey = value;
+
+ /** Register the active service provider on the 'AfterSaveMessage' callback.
+ * So the registered provider will be invoked when a message is saved.
+ * All the other inactive service provider must be deactivated.
+ */
+ settings.get('AutoTranslate_ServiceProvider', (key, value) => {
+ if (this.name === value) {
+ this.registerAfterSaveMsgCallBack(this.name);
+ } else {
+ this.unRegisterAfterSaveMsgCallBack(this.name);
+ }
});
}
+ /**
+ * Extracts non-translatable parts of a message
+ * @param {object} message
+ * @return {object} message
+ */
tokenize(message) {
if (!message.tokens || !Array.isArray(message.tokens)) {
message.tokens = [];
@@ -93,10 +152,12 @@ class AutoTranslate {
tokenizeCode(message) {
let count = message.tokens.length;
-
message.html = message.msg;
message = Markdown.parseMessageNotEscaped(message);
- message.msg = message.html;
+
+ // Some parsers (e. g. Marked) wrap the complete message in a - this is unnecessary and should be ignored with respect to translations
+ const regexWrappedParagraph = new RegExp('^\s*
|<\/p>\s*$', 'gm');
+ message.msg = message.msg.replace(regexWrappedParagraph, '');
for (const tokenIndex in message.tokens) {
if (message.tokens.hasOwnProperty(tokenIndex)) {
@@ -153,8 +214,17 @@ class AutoTranslate {
return message.msg;
}
+ /**
+ * Triggers the translation of the prepared (tokenized) message
+ * and persists the result
+ * @public
+ * @param {object} message
+ * @param {object} room
+ * @param {object} targetLanguage
+ * @returns {object} unmodified message object.
+ */
translateMessage(message, room, targetLanguage) {
- if (this.enabled && this.apiKey) {
+ if (this.autoTranslateEnabled && this.apiKey) {
let targetLanguages;
if (targetLanguage) {
targetLanguages = [targetLanguage];
@@ -163,35 +233,13 @@ class AutoTranslate {
}
if (message.msg) {
Meteor.defer(() => {
- const translations = {};
let targetMessage = Object.assign({}, message);
-
targetMessage.html = s.escapeHTML(String(targetMessage.msg));
targetMessage = this.tokenize(targetMessage);
- let msgs = targetMessage.msg.split('\n');
- msgs = msgs.map((msg) => encodeURIComponent(msg));
- const query = `q=${ msgs.join('&q=') }`;
-
- const supportedLanguages = this.getSupportedLanguages('en');
- targetLanguages.forEach((language) => {
- if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
- language = language.substr(0, 2);
- }
- let result;
- try {
- result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query });
- } catch (e) {
- console.log('Error translating message', e);
- return message;
- }
- if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) {
- const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n');
- translations[language] = this.deTokenize(Object.assign({}, targetMessage, { msg: txt }));
- }
- });
+ const translations = this._translateMessage(targetMessage, targetLanguages);
if (!_.isEmpty(translations)) {
- Messages.addTranslations(message._id, translations);
+ Messages.addTranslations(message._id, translations, TranslationProviderRegistry._activeProvider);
}
});
}
@@ -201,20 +249,8 @@ class AutoTranslate {
for (const index in message.attachments) {
if (message.attachments.hasOwnProperty(index)) {
const attachment = message.attachments[index];
- const translations = {};
if (attachment.description || attachment.text) {
- const query = `q=${ encodeURIComponent(attachment.description || attachment.text) }`;
- const supportedLanguages = this.getSupportedLanguages('en');
- targetLanguages.forEach((language) => {
- if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
- language = language.substr(0, 2);
- }
- const result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query });
- if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) {
- const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n');
- translations[language] = txt;
- }
- });
+ const translations = this._translateAttachmentDescriptions(attachment, targetLanguages);
if (!_.isEmpty(translations)) {
Messages.addAttachmentTranslations(message._id, index, translations);
}
@@ -227,37 +263,76 @@ class AutoTranslate {
return message;
}
- getSupportedLanguages(target) {
- if (this.enabled && this.apiKey) {
- if (this.supportedLanguages[target]) {
- return this.supportedLanguages[target];
- }
+ /**
+ * On changing the service provider, the callback in which the translation
+ * is being requested needs to be switched to the new provider
+ * @protected
+ * @param {string} provider
+ */
+ registerAfterSaveMsgCallBack(provider) {
+ callbacks.add('afterSaveMessage', this.translateMessage.bind(this), callbacks.priority.MEDIUM, provider);
+ }
- let result;
- const params = { key: this.apiKey };
- if (target) {
- params.target = target;
- }
+ /**
+ * On changing the service provider, the callback in which the translation
+ * is being requested needs to be deactivated for the all other translation providers
+ * @protected
+ * @param {string} provider
+ */
+ unRegisterAfterSaveMsgCallBack(provider) {
+ callbacks.remove('afterSaveMessage', provider);
+ }
- if (this.supportedLanguages[target]) {
- return this.supportedLanguages[target];
- }
+ /**
+ * Returns metadata information about the service provider which is used by
+ * the generic implementation
+ * @abstract
+ * @protected
+ * @returns { name, displayName, settings }
+ };
+ */
+ _getProviderMetadata() {
+ Logger.warn('must be implemented by subclass!', '_getProviderMetadata');
+ }
- try {
- result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params });
- } catch (e) {
- if (e.response && e.response.statusCode === 400 && e.response.data && e.response.data.error && e.response.data.error.status === 'INVALID_ARGUMENT') {
- params.target = 'en';
- target = 'en';
- if (!this.supportedLanguages[target]) {
- result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params });
- }
- }
- }
- this.supportedLanguages[target || 'en'] = result && result.data && result.data.data && result.data.data.languages;
- return this.supportedLanguages[target || 'en'];
- }
+
+ /**
+ * Provides the possible languages _from_ which a message can be translated into a target language
+ * @abstract
+ * @protected
+ * @param {string} target - the language into which shall be translated
+ * @returns [{ language, name }]
+ */
+ getSupportedLanguages(target) {
+ Logger.warn('must be implemented by subclass!', 'getSupportedLanguages', target);
+ }
+
+ /**
+ * Performs the actual translation of a message,
+ * usually by sending a REST API call to the service provider.
+ * @abstract
+ * @protected
+ * @param {object} message
+ * @param {object} targetLanguages
+ * @return {object}
+ */
+ _translateMessage(message, targetLanguages) {
+ Logger.warn('must be implemented by subclass!', '_translateMessage', message, targetLanguages);
+ }
+
+ /**
+ * Performs the actual translation of an attachment (precisely its description),
+ * usually by sending a REST API call to the service provider.
+ * @abstract
+ * @param {object} attachment
+ * @param {object} targetLanguages
+ * @returns {object} translated messages for each target language
+ */
+ _translateAttachmentDescriptions(attachment, targetLanguages) {
+ Logger.warn('must be implemented by subclass!', '_translateAttachmentDescriptions', attachment, targetLanguages);
}
}
-export default new AutoTranslate();
+Meteor.startup(() => {
+ TranslationProviderRegistry.loadActiveServiceProvider();
+});
diff --git a/app/autotranslate/server/deeplTranslate.js b/app/autotranslate/server/deeplTranslate.js
new file mode 100644
index 000000000000..81227e576a41
--- /dev/null
+++ b/app/autotranslate/server/deeplTranslate.js
@@ -0,0 +1,196 @@
+/**
+ * @author Vigneshwaran Odayappan
+ */
+
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import { HTTP } from 'meteor/http';
+import _ from 'underscore';
+
+import { TranslationProviderRegistry, AutoTranslate } from './autotranslate';
+import { SystemLogger } from '../../logger/server';
+import { settings } from '../../settings';
+
+/**
+ * DeepL translation service provider class representation.
+ * Encapsulates the service provider settings and information.
+ * Provides languages supported by the service provider.
+ * Resolves API call to service provider to resolve the translation request.
+ * @class
+ * @augments AutoTranslate
+ */
+class DeeplAutoTranslate extends AutoTranslate {
+ /**
+ * setup api reference to deepl translate to be used as message translation provider.
+ * @constructor
+ */
+ constructor() {
+ super();
+ this.name = 'deepl-translate';
+ this.apiEndPointUrl = 'https://api.deepl.com/v1/translate';
+ // Get the service provide API key.
+ settings.get('AutoTranslate_DeepLAPIKey', (key, value) => {
+ this.apiKey = value;
+ });
+ }
+
+ /**
+ * Returns metadata information about the service provide
+ * @private implements super abstract method.
+ * @return {object}
+ */
+ _getProviderMetadata() {
+ return {
+ name: this.name,
+ displayName: TAPi18n.__('AutoTranslate_DeepL'),
+ settings: this._getSettings(),
+ };
+ }
+
+ /**
+ * Returns necessary settings information about the translation service provider.
+ * @private implements super abstract method.
+ * @return {object}
+ */
+ _getSettings() {
+ return {
+ apiKey: this.apiKey,
+ apiEndPointUrl: this.apiEndPointUrl,
+ };
+ }
+
+ /**
+ * Returns supported languages for translation by the active service provider.
+ * Deepl does not provide an endpoint yet to retrieve the supported languages.
+ * So each supported languages are explicitly maintained.
+ * @private implements super abstract method.
+ * @param {string} target
+ * @returns {object} code : value pair
+ */
+ getSupportedLanguages(target) {
+ if (this.autoTranslateEnabled && this.apiKey) {
+ if (this.supportedLanguages[target]) {
+ return this.supportedLanguages[target];
+ }
+ this.supportedLanguages[target] = [
+ {
+ language: 'en',
+ name: TAPi18n.__('Language_English', { lng: target }),
+ },
+ {
+ language: 'de',
+ name: TAPi18n.__('Language_German', { lng: target }),
+ },
+ {
+ language: 'fr',
+ name: TAPi18n.__('Language_French', { lng: target }),
+ },
+ {
+ language: 'es',
+ name: TAPi18n.__('Language_Spanish', { lng: target }),
+ },
+ {
+ language: 'it',
+ name: TAPi18n.__('Language_Italian', { lng: target }),
+ },
+ {
+ language: 'nl',
+ name: TAPi18n.__('Language_Dutch', { lng: target }),
+ },
+ {
+ language: 'pl',
+ name: TAPi18n.__('Language_Polish', { lng: target }),
+ },
+ {
+ language: 'pt',
+ name: TAPi18n.__('Language_Portuguese', { lng: target }),
+ },
+ {
+ language: 'ru',
+ name: TAPi18n.__('Language_Russian', { lng: target }),
+ },
+ ];
+
+ return this.supportedLanguages[target];
+ }
+ }
+
+ /**
+ * Send Request REST API call to the service provider.
+ * Returns translated message for each target language in target languages.
+ * @private
+ * @param {object} message
+ * @param {object} targetLanguages
+ * @returns {object} translations: Translated messages for each language
+ */
+ _translateMessage(message, targetLanguages) {
+ const translations = {};
+ let msgs = message.msg.split('\n');
+ msgs = msgs.map((msg) => encodeURIComponent(msg));
+ const query = `text=${ msgs.join('&text=') }`;
+ const supportedLanguages = this.getSupportedLanguages('en');
+ targetLanguages.forEach((language) => {
+ if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
+ language = language.substr(0, 2);
+ }
+ try {
+ const result = HTTP.get(this.apiEndPointUrl, {
+ params: {
+ auth_key: this.apiKey,
+ target_lang: language,
+ },
+ query,
+ });
+
+ if (result.statusCode === 200 && result.data && result.data.translations && Array.isArray(result.data.translations) && result.data.translations.length > 0) {
+ // store translation only when the source and target language are different.
+ // multiple lines might contain different languages => Mix the text between source and detected target if neccessary
+ const translatedText = result.data.translations
+ .map((translation, index) => (translation.detected_source_language !== language ? translation.text : msgs[index]))
+ .join('\n');
+ translations[language] = this.deTokenize(Object.assign({}, message, { msg: translatedText }));
+ }
+ } catch (e) {
+ SystemLogger.error('Error translating message', e);
+ }
+ });
+ return translations;
+ }
+
+ /**
+ * Returns translated message attachment description in target languages.
+ * @private
+ * @param {object} attachment
+ * @param {object} targetLanguages
+ * @returns {object} translated messages for each target language
+ */
+ _translateAttachmentDescriptions(attachment, targetLanguages) {
+ const translations = {};
+ const query = `text=${ encodeURIComponent(attachment.description || attachment.text) }`;
+ const supportedLanguages = this.getSupportedLanguages('en');
+ targetLanguages.forEach((language) => {
+ if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
+ language = language.substr(0, 2);
+ }
+ try {
+ const result = HTTP.get(this.apiEndPointUrl, {
+ params: {
+ auth_key: this.apiKey,
+ target_lang: language,
+ },
+ query,
+ });
+ if (result.statusCode === 200 && result.data && result.data.translations && Array.isArray(result.data.translations) && result.data.translations.length > 0) {
+ if (result.data.translations.map((translation) => translation.detected_source_language).join() !== language) {
+ translations[language] = result.data.translations.map((translation) => translation.text);
+ }
+ }
+ } catch (e) {
+ SystemLogger.error('Error translating message attachment', e);
+ }
+ });
+ return translations;
+ }
+}
+
+// Register DeepL translation provider to the registry.
+TranslationProviderRegistry.registerProvider(new DeeplAutoTranslate());
diff --git a/app/autotranslate/server/googleTranslate.js b/app/autotranslate/server/googleTranslate.js
new file mode 100644
index 000000000000..7486ff118599
--- /dev/null
+++ b/app/autotranslate/server/googleTranslate.js
@@ -0,0 +1,188 @@
+/**
+ * @author Vigneshwaran Odayappan
+ */
+
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import { HTTP } from 'meteor/http';
+import _ from 'underscore';
+
+import { AutoTranslate, TranslationProviderRegistry } from './autotranslate';
+import { SystemLogger } from '../../logger/server';
+import { settings } from '../../settings';
+
+/**
+ * Represents google translate class
+ * @class
+ * @augments AutoTranslate
+ */
+class GoogleAutoTranslate extends AutoTranslate {
+ /**
+ * setup api reference to Google translate to be used as message translation provider.
+ * @constructor
+ */
+ constructor() {
+ super();
+ this.name = 'google-translate';
+ this.apiEndPointUrl = 'https://translation.googleapis.com/language/translate/v2';
+ // Get the service provide API key.
+ settings.get('AutoTranslate_GoogleAPIKey', (key, value) => {
+ this.apiKey = value;
+ });
+ }
+
+ /**
+ * Returns metadata information about the service provider
+ * @private implements super abstract method.
+ * @returns {object}
+ */
+ _getProviderMetadata() {
+ return {
+ name: this.name,
+ displayName: TAPi18n.__('AutoTranslate_Google'),
+ settings: this._getSettings(),
+ };
+ }
+
+ /**
+ * Returns necessary settings information about the translation service provider.
+ * @private implements super abstract method.
+ * @returns {object}
+ */
+ _getSettings() {
+ return {
+ apiKey: this.apiKey,
+ apiEndPointUrl: this.apiEndPointUrl,
+ };
+ }
+
+ /**
+ * Returns supported languages for translation by the active service provider.
+ * Google Translate api provides the list of supported languages.
+ * @private implements super abstract method.
+ * @param {string} target : user language setting or 'en'
+ * @returns {object} code : value pair
+ */
+ getSupportedLanguages(target) {
+ let supportedLanguages = {};
+ if (this.autoTranslateEnabled && this.apiKey) {
+ if (this.supportedLanguages[target]) {
+ return this.supportedLanguages[target];
+ }
+
+ let result;
+ const params = {
+ key: this.apiKey,
+ };
+
+ if (target) {
+ params.target = target;
+ }
+
+ try {
+ result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', {
+ params,
+ });
+ } catch (e) {
+ // Fallback: Get the English names of the target languages
+ if (e.response && e.response.statusCode === 400 && e.response.data && e.response.data.error && e.response.data.error.status === 'INVALID_ARGUMENT') {
+ params.target = 'en';
+ target = 'en';
+ if (!this.supportedLanguages[target]) {
+ result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', {
+ params,
+ });
+ }
+ }
+ }
+
+ if (this.supportedLanguages[target]) {
+ supportedLanguages = this.supportedLanguages[target];
+ } else {
+ this.supportedLanguages[target || 'en'] = result && result.data && result.data.data && result.data.data.languages;
+ supportedLanguages = this.supportedLanguages[target || 'en'];
+ }
+
+ return supportedLanguages;
+ }
+ }
+
+ /**
+ * Send Request REST API call to the service provider.
+ * Returns translated message for each target language in target languages.
+ * @private
+ * @param {object} message
+ * @param {object} targetLanguages
+ * @returns {object} translations: Translated messages for each language
+ */
+ _translateMessage(message, targetLanguages) {
+ const translations = {};
+ let msgs = message.msg.split('\n');
+ msgs = msgs.map((msg) => encodeURIComponent(msg));
+
+ const query = `q=${ msgs.join('&q=') }`;
+ const supportedLanguages = this.getSupportedLanguages('en');
+
+ targetLanguages.forEach((language) => {
+ if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
+ language = language.substr(0, 2);
+ }
+
+ try {
+ const result = HTTP.get(this.apiEndPointUrl, {
+ params: {
+ key: this.apiKey,
+ target: language,
+ },
+ query,
+ });
+
+ if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) {
+ const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n');
+ translations[language] = this.deTokenize(Object.assign({}, message, { msg: txt }));
+ }
+ } catch (e) {
+ SystemLogger.error('Error translating message', e);
+ }
+ });
+ return translations;
+ }
+
+ /**
+ * Returns translated message attachment description in target languages.
+ * @private
+ * @param {object} attachment
+ * @param {object} targetLanguages
+ * @returns {object} translated attachment descriptions for each target language
+ */
+ _translateAttachmentDescriptions(attachment, targetLanguages) {
+ const translations = {};
+ const query = `q=${ encodeURIComponent(attachment.description || attachment.text) }`;
+ const supportedLanguages = this.getSupportedLanguages('en');
+
+ targetLanguages.forEach((language) => {
+ if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) {
+ language = language.substr(0, 2);
+ }
+
+ try {
+ const result = HTTP.get(this.apiEndPointUrl, {
+ params: {
+ key: this.apiKey,
+ target: language,
+ },
+ query,
+ });
+
+ if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) {
+ translations[language] = result.data.data.translations.map((translation) => translation.translatedText).join('\n');
+ }
+ } catch (e) {
+ SystemLogger.error('Error translating message', e);
+ }
+ });
+ return translations;
+ }
+}
+
+// Register Google translation provider.
+TranslationProviderRegistry.registerProvider(new GoogleAutoTranslate());
diff --git a/app/autotranslate/server/index.js b/app/autotranslate/server/index.js
index a3692f29c722..186115b06c6d 100644
--- a/app/autotranslate/server/index.js
+++ b/app/autotranslate/server/index.js
@@ -1,6 +1,21 @@
+/* eslint-disable import/no-duplicates */
+/**
+ * This file contains the exported members of the package shall be re-used.
+ * @module AutoTranslate, TranslationProviderRegistry
+ */
+
+import { AutoTranslate, TranslationProviderRegistry } from './autotranslate';
import './settings';
import './permissions';
import './autotranslate';
import './methods/getSupportedLanguages';
import './methods/saveSettings';
import './methods/translateMessage';
+import './googleTranslate.js';
+import './deeplTranslate.js';
+import './methods/getProviderUiMetadata.js';
+
+export {
+ AutoTranslate,
+ TranslationProviderRegistry,
+};
diff --git a/app/autotranslate/server/methods/getProviderUiMetadata.js b/app/autotranslate/server/methods/getProviderUiMetadata.js
new file mode 100644
index 000000000000..7446cd5d3766
--- /dev/null
+++ b/app/autotranslate/server/methods/getProviderUiMetadata.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+
+import { TranslationProviderRegistry } from '../autotranslate';
+
+Meteor.methods({
+ 'autoTranslate.getProviderUiMetadata'() {
+ const providersMetadata = {};
+
+ if (!Meteor.userId()) {
+ throw new Meteor.Error('error-action-not-allowed', 'Login neccessary', { method: 'autoTranslate.getProviderUiMetadata' });
+ }
+
+ for (const provider in TranslationProviderRegistry._providers) {
+ if (TranslationProviderRegistry._providers.hasOwnProperty(provider)) {
+ const { name, displayName } = TranslationProviderRegistry._providers[provider]._getProviderMetadata();
+ providersMetadata[provider] = { name, displayName };
+ }
+ }
+
+ return providersMetadata;
+ },
+});
diff --git a/app/autotranslate/server/methods/getSupportedLanguages.js b/app/autotranslate/server/methods/getSupportedLanguages.js
index 2f44877a7e1c..4780fa39f7e7 100644
--- a/app/autotranslate/server/methods/getSupportedLanguages.js
+++ b/app/autotranslate/server/methods/getSupportedLanguages.js
@@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { hasPermission } from '../../../authorization';
-import AutoTranslate from '../autotranslate';
+
+import { TranslationProviderRegistry } from '..';
Meteor.methods({
'autoTranslate.getSupportedLanguages'(targetLanguage) {
@@ -10,7 +11,7 @@ Meteor.methods({
throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { method: 'autoTranslate.saveSettings' });
}
- return AutoTranslate.getSupportedLanguages(targetLanguage);
+ return TranslationProviderRegistry.getActiveProvider().getSupportedLanguages(targetLanguage);
},
});
diff --git a/app/autotranslate/server/methods/translateMessage.js b/app/autotranslate/server/methods/translateMessage.js
index 26ea7cf301a1..0c9c4836334f 100644
--- a/app/autotranslate/server/methods/translateMessage.js
+++ b/app/autotranslate/server/methods/translateMessage.js
@@ -1,13 +1,14 @@
import { Meteor } from 'meteor/meteor';
import { Rooms } from '../../../models';
-import AutoTranslate from '../autotranslate';
+
+import { TranslationProviderRegistry } from '..';
Meteor.methods({
'autoTranslate.translateMessage'(message, targetLanguage) {
const room = Rooms.findOneById(message && message.rid);
- if (message && room && AutoTranslate) {
- return AutoTranslate.translateMessage(message, room, targetLanguage);
+ if (message && room && TranslationProviderRegistry) {
+ TranslationProviderRegistry.getActiveProvider().translateMessage(message, room, targetLanguage);
}
},
});
diff --git a/app/autotranslate/server/settings.js b/app/autotranslate/server/settings.js
index 02931fd38771..08e8f6c92f0b 100644
--- a/app/autotranslate/server/settings.js
+++ b/app/autotranslate/server/settings.js
@@ -3,6 +3,55 @@ import { Meteor } from 'meteor/meteor';
import { settings } from '../../settings';
Meteor.startup(function() {
- settings.add('AutoTranslate_Enabled', false, { type: 'boolean', group: 'Message', section: 'AutoTranslate', public: true });
- settings.add('AutoTranslate_GoogleAPIKey', '', { type: 'string', group: 'Message', section: 'AutoTranslate', enableQuery: { _id: 'AutoTranslate_Enabled', value: true }, secret: true });
+ settings.add('AutoTranslate_Enabled', false, {
+ type: 'boolean',
+ group: 'Message',
+ section: 'AutoTranslate',
+ public: true,
+ });
+
+ settings.add('AutoTranslate_ServiceProvider', 'google-translate', {
+ type: 'select',
+ group: 'Message',
+ section: 'AutoTranslate',
+ values: [{
+ key: 'google-translate',
+ i18nLabel: 'AutoTranslate_Google',
+ }, {
+ key: 'deepl-translate',
+ i18nLabel: 'AutoTranslate_DeepL',
+ }],
+ enableQuery: [{ _id: 'AutoTranslate_Enabled', value: true }],
+ i18nLabel: 'AutoTranslate_ServiceProvider',
+ public: true,
+ });
+
+ settings.add('AutoTranslate_GoogleAPIKey', '', {
+ type: 'string',
+ group: 'Message',
+ section: 'AutoTranslate_Google',
+ public: true,
+ i18nLabel: 'AutoTranslate_APIKey',
+ enableQuery: [
+ {
+ _id: 'AutoTranslate_Enabled', value: true,
+ },
+ {
+ _id: 'AutoTranslate_ServiceProvider', value: 'google-translate',
+ }],
+ });
+
+ settings.add('AutoTranslate_DeepLAPIKey', '', {
+ type: 'string',
+ group: 'Message',
+ section: 'AutoTranslate_DeepL',
+ public: true,
+ i18nLabel: 'AutoTranslate_APIKey',
+ enableQuery: [
+ {
+ _id: 'AutoTranslate_Enabled', value: true,
+ }, {
+ _id: 'AutoTranslate_ServiceProvider', value: 'deepl-translate',
+ }],
+ });
});
diff --git a/app/channel-settings/client/views/channelSettings.js b/app/channel-settings/client/views/channelSettings.js
index 04d010a3394d..cb3e4379ad76 100644
--- a/app/channel-settings/client/views/channelSettings.js
+++ b/app/channel-settings/client/views/channelSettings.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import moment from 'moment';
import s from 'underscore.string';
diff --git a/app/channel-settings/server/functions/saveRoomType.js b/app/channel-settings/server/functions/saveRoomType.js
index 6831e239e9d8..f2a5948b8d32 100644
--- a/app/channel-settings/server/functions/saveRoomType.js
+++ b/app/channel-settings/server/functions/saveRoomType.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { callbacks } from '../../../callbacks';
import { Rooms, Subscriptions, Messages } from '../../../models';
diff --git a/app/chatpal-search/client/template/admin.js b/app/chatpal-search/client/template/admin.js
index bcbf71c873e6..625d8c4b2e0f 100644
--- a/app/chatpal-search/client/template/admin.js
+++ b/app/chatpal-search/client/template/admin.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import { settings } from '../../../settings';
diff --git a/app/chatpal-search/client/template/result.js b/app/chatpal-search/client/template/result.js
index 08c3512905d5..2ef230fccc8a 100644
--- a/app/chatpal-search/client/template/result.js
+++ b/app/chatpal-search/client/template/result.js
@@ -1,7 +1,7 @@
import { ReactiveVar } from 'meteor/reactive-var';
import { Session } from 'meteor/session';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { DateFormat } from '../../../lib';
import { roomTypes, getURL } from '../../../utils';
diff --git a/app/custom-oauth/server/custom_oauth_server.js b/app/custom-oauth/server/custom_oauth_server.js
index 5becea434c06..660685296f9c 100644
--- a/app/custom-oauth/server/custom_oauth_server.js
+++ b/app/custom-oauth/server/custom_oauth_server.js
@@ -365,7 +365,7 @@ export class CustomOAuth {
}
if (serviceData.username) {
- const user = Users.findOneByUsernameIgnoringCase(serviceData.username);
+ const user = Users.findOneByUsernameAndServiceNameIgnoringCase(serviceData.username, serviceName);
if (!user) {
return;
}
@@ -430,10 +430,9 @@ export class CustomOAuth {
check(options, Match.ObjectIncluding({
accessToken: String,
expiresIn: Match.Integer,
- identity: Match.Maybe(Object),
}));
- const identity = options.identity || self.getIdentity(options.accessToken);
+ const identity = self.getIdentity(options.accessToken);
const serviceData = {
accessToken: options.accessToken,
diff --git a/app/custom-sounds/client/admin/soundEdit.js b/app/custom-sounds/client/admin/soundEdit.js
index f28aa360b421..c55f2f5a03d3 100644
--- a/app/custom-sounds/client/admin/soundEdit.js
+++ b/app/custom-sounds/client/admin/soundEdit.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import s from 'underscore.string';
diff --git a/app/discussion/client/views/creationDialog/CreateDiscussion.js b/app/discussion/client/views/creationDialog/CreateDiscussion.js
index a786507c330f..2ca7b1c854d8 100755
--- a/app/discussion/client/views/creationDialog/CreateDiscussion.js
+++ b/app/discussion/client/views/creationDialog/CreateDiscussion.js
@@ -3,7 +3,7 @@ import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { AutoComplete } from 'meteor/mizzao:autocomplete';
import { Blaze } from 'meteor/blaze';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import { roomTypes } from '../../../../utils/client';
@@ -29,7 +29,7 @@ Template.CreateDiscussion.helpers({
},
createIsDisabled() {
const { parentChannel, discussionName } = Template.instance();
- return parentChannel.get() && discussionName.get() ? '' : 'disabled';
+ return parentChannel.get() && discussionName.get().trim() ? '' : 'disabled';
},
parentChannel() {
const instance = Template.instance();
diff --git a/app/e2e/client/rocketchat.e2e.js b/app/e2e/client/rocketchat.e2e.js
index 243713b9fa91..536d1b5a495f 100644
--- a/app/e2e/client/rocketchat.e2e.js
+++ b/app/e2e/client/rocketchat.e2e.js
@@ -4,7 +4,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { EJSON } from 'meteor/ejson';
import { FlowRouter } from 'meteor/kadira:flow-router';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { E2ERoom } from './rocketchat.e2e.room';
import {
diff --git a/app/emoji-custom/client/admin/adminEmoji.js b/app/emoji-custom/client/admin/adminEmoji.js
index 7a46f4c9cb0c..8f1ba6d8f3ba 100644
--- a/app/emoji-custom/client/admin/adminEmoji.js
+++ b/app/emoji-custom/client/admin/adminEmoji.js
@@ -48,7 +48,7 @@ Template.adminEmoji.helpers({
if ((currentTarget.offsetHeight + currentTarget.scrollTop) < (currentTarget.scrollHeight - 100)) {
return;
}
- if (Template.instance().limit.get() > Template.instance().customemoji().length) {
+ if (instance.limit.get() > instance.customemoji().length) {
return false;
}
instance.limit.set(instance.limit.get() + 50);
diff --git a/app/emoji-custom/client/admin/emojiEdit.js b/app/emoji-custom/client/admin/emojiEdit.js
index b226ea27c470..09404693aa5e 100644
--- a/app/emoji-custom/client/admin/emojiEdit.js
+++ b/app/emoji-custom/client/admin/emojiEdit.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import s from 'underscore.string';
diff --git a/app/emoji/client/emojiParser.js b/app/emoji/client/emojiParser.js
index 00ef84402bee..d7f7980df11a 100644
--- a/app/emoji/client/emojiParser.js
+++ b/app/emoji/client/emojiParser.js
@@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { getUserPreference } from '../../utils';
+import { isIE11 } from '../../ui-utils/client/lib/isIE11';
import { callbacks } from '../../callbacks';
import { emoji } from '../lib/rocketchat';
@@ -32,41 +33,44 @@ Tracker.autorun(() => {
const emojis = Array.from(checkEmojiOnly.querySelectorAll('.emoji:not(:empty), .emojione:not(:empty)'));
- const walker = document.createTreeWalker(
- checkEmojiOnly,
- NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
- {
- acceptNode: (node) => {
- if (node.nodeType === Node.ELEMENT_NODE && (
- node.classList.contains('emojione')
- || node.classList.contains('emoji')
- )) {
- return NodeFilter.FILTER_REJECT;
- }
- return NodeFilter.FILTER_ACCEPT;
- },
- },
- );
-
let hasText = false;
- while (walker.nextNode()) {
- if (walker.currentNode.nodeType === Node.TEXT_NODE && walker.currentNode.nodeValue.trim() !== '') {
- hasText = true;
- break;
+ if (!isIE11()) {
+ const filter = (node) => {
+ if (node.nodeType === Node.ELEMENT_NODE && (
+ node.classList.contains('emojione')
+ || node.classList.contains('emoji')
+ )) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ return NodeFilter.FILTER_ACCEPT;
+ };
+
+ const walker = document.createTreeWalker(
+ checkEmojiOnly,
+ NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
+ filter
+ );
+
+
+ while (walker.nextNode()) {
+ if (walker.currentNode.nodeType === Node.TEXT_NODE && walker.currentNode.nodeValue.trim() !== '') {
+ hasText = true;
+ break;
+ }
}
- }
-
- const emojiOnly = emojis.length && !hasText;
-
- if (emojiOnly) {
- for (let i = 0, len = emojis.length; i < len; i++) {
- const { classList } = emojis[i];
- classList.add('big');
+ const emojiOnly = emojis.length && !hasText;
+
+ if (emojiOnly) {
+ for (let i = 0, len = emojis.length; i < len; i++) {
+ const { classList } = emojis[i];
+ classList.add('big');
+ }
+ html = checkEmojiOnly.innerHTML;
}
- html = checkEmojiOnly.innerHTML;
}
+
// apostrophe (') back to '
html = html.replace(/\'/g, ''');
diff --git a/app/file-upload/server/config/AmazonS3.js b/app/file-upload/server/config/AmazonS3.js
index c262351fc0da..48aea6aea167 100644
--- a/app/file-upload/server/config/AmazonS3.js
+++ b/app/file-upload/server/config/AmazonS3.js
@@ -8,23 +8,21 @@ import { FileUploadClass, FileUpload } from '../lib/FileUpload';
import '../../ufs/AmazonS3/server.js';
const get = function(file, req, res) {
- const fileUrl = this.store.getRedirectURL(file);
+ const forceDownload = typeof req.query.download !== 'undefined';
+ const fileUrl = this.store.getRedirectURL(file, forceDownload);
- if (fileUrl) {
- const storeType = file.store.split(':').pop();
- if (settings.get(`FileUpload_S3_Proxy_${ storeType }`)) {
- const request = /^https:/.test(fileUrl) ? https : http;
- request.get(fileUrl, (fileRes) => fileRes.pipe(res));
- } else {
- res.removeHeader('Content-Length');
- res.removeHeader('Cache-Control');
- res.setHeader('Location', fileUrl);
- res.writeHead(302);
- res.end();
- }
- } else {
- res.end();
+ if (!fileUrl) {
+ return res.end();
+ }
+
+ const storeType = file.store.split(':').pop();
+ if (settings.get(`FileUpload_S3_Proxy_${ storeType }`)) {
+ const request = /^https:/.test(fileUrl) ? https : http;
+
+ return FileUpload.proxyFile(file.name, fileUrl, forceDownload, request, req, res);
}
+
+ return FileUpload.redirectToFile(fileUrl, req, res);
};
const copy = function(file, out) {
diff --git a/app/file-upload/server/config/GoogleStorage.js b/app/file-upload/server/config/GoogleStorage.js
index 0af606ead1f0..c8e11bfe1fb5 100644
--- a/app/file-upload/server/config/GoogleStorage.js
+++ b/app/file-upload/server/config/GoogleStorage.js
@@ -8,31 +8,30 @@ import { settings } from '../../../settings';
import '../../ufs/GoogleStorage/server.js';
const get = function(file, req, res) {
- this.store.getRedirectURL(file, (err, fileUrl) => {
+ const forceDownload = typeof req.query.download !== 'undefined';
+
+ this.store.getRedirectURL(file, forceDownload, (err, fileUrl) => {
if (err) {
- console.error(err);
+ return console.error(err);
}
- if (fileUrl) {
- const storeType = file.store.split(':').pop();
- if (settings.get(`FileUpload_GoogleStorage_Proxy_${ storeType }`)) {
- const request = /^https:/.test(fileUrl) ? https : http;
- request.get(fileUrl, (fileRes) => fileRes.pipe(res));
- } else {
- res.removeHeader('Content-Length');
- res.removeHeader('Cache-Control');
- res.setHeader('Location', fileUrl);
- res.writeHead(302);
- res.end();
- }
- } else {
- res.end();
+ if (!fileUrl) {
+ return res.end();
}
+
+ const storeType = file.store.split(':').pop();
+ if (settings.get(`FileUpload_GoogleStorage_Proxy_${ storeType }`)) {
+ const request = /^https:/.test(fileUrl) ? https : http;
+
+ return FileUpload.proxyFile(file.name, fileUrl, forceDownload, request, req, res);
+ }
+
+ return FileUpload.redirectToFile(fileUrl, req, res);
});
};
const copy = function(file, out) {
- this.store.getRedirectURL(file, (err, fileUrl) => {
+ this.store.getRedirectURL(file, false, (err, fileUrl) => {
if (err) {
console.error(err);
}
diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js
index 9258da3fcd1e..ee59c0b65a79 100644
--- a/app/file-upload/server/lib/FileUpload.js
+++ b/app/file-upload/server/lib/FileUpload.js
@@ -8,7 +8,7 @@ import sharp from 'sharp';
import { Cookies } from 'meteor/ostrio:cookies';
import { UploadFS } from 'meteor/jalik:ufs';
import { Match } from 'meteor/check';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import filesize from 'filesize';
import { settings } from '../../../settings/server';
@@ -39,6 +39,10 @@ settings.get('FileUpload_MaxFileSize', function(key, value) {
export const FileUpload = {
handlers: {},
+ getPath(path = '') {
+ return `/file-upload/${ path }`;
+ },
+
configureUploadsStore(store, name, options) {
const type = name.split(':').pop();
const stores = UploadFS.getStores();
@@ -371,6 +375,20 @@ export const FileUpload = {
return false;
},
+
+ redirectToFile(fileUrl, req, res) {
+ res.removeHeader('Content-Length');
+ res.removeHeader('Cache-Control');
+ res.setHeader('Location', fileUrl);
+ res.writeHead(302);
+ res.end();
+ },
+
+ proxyFile(fileName, fileUrl, forceDownload, request, req, res) {
+ res.setHeader('Content-Disposition', `${ forceDownload ? 'attachment' : 'inline' }; filename="${ encodeURI(fileName) }"`);
+
+ request.get(fileUrl, (fileRes) => fileRes.pipe(res));
+ },
};
export class FileUploadClass {
diff --git a/app/file-upload/server/lib/requests.js b/app/file-upload/server/lib/requests.js
index 039231a0d5c4..80a3b4213b38 100644
--- a/app/file-upload/server/lib/requests.js
+++ b/app/file-upload/server/lib/requests.js
@@ -3,7 +3,7 @@ import { WebApp } from 'meteor/webapp';
import { FileUpload } from './FileUpload';
import { Uploads } from '../../../models';
-WebApp.connectHandlers.use('/file-upload/', function(req, res, next) {
+WebApp.connectHandlers.use(FileUpload.getPath(), function(req, res, next) {
const match = /^\/([^\/]+)\/(.*)/.exec(req.url);
if (match && match[1]) {
diff --git a/app/file-upload/server/methods/sendFileMessage.js b/app/file-upload/server/methods/sendFileMessage.js
index cdf9aef166f9..6d17cfaf1a06 100644
--- a/app/file-upload/server/methods/sendFileMessage.js
+++ b/app/file-upload/server/methods/sendFileMessage.js
@@ -30,7 +30,7 @@ Meteor.methods({
Uploads.updateFileComplete(file._id, Meteor.userId(), _.omit(file, '_id'));
- const fileUrl = `/file-upload/${ file._id }/${ encodeURI(file.name) }`;
+ const fileUrl = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`);
const attachment = {
title: file.name,
diff --git a/app/file-upload/ufs/AmazonS3/server.js b/app/file-upload/ufs/AmazonS3/server.js
index 656bfb2c0656..a22014a73904 100644
--- a/app/file-upload/ufs/AmazonS3/server.js
+++ b/app/file-upload/ufs/AmazonS3/server.js
@@ -47,10 +47,11 @@ export class AmazonS3Store extends UploadFS.Store {
}
};
- this.getRedirectURL = function(file) {
+ this.getRedirectURL = function(file, forceDownload = false) {
const params = {
Key: this.getPath(file),
Expires: classOptions.URLExpiryTimeSpan,
+ ResponseContentDisposition: `${ forceDownload ? 'attachment' : 'inline' }; filename="${ encodeURI(file.name) }"`,
};
return s3.getSignedUrl('getObject', params);
@@ -140,7 +141,6 @@ export class AmazonS3Store extends UploadFS.Store {
Key: this.getPath(file),
Body: writeStream,
ContentType: file.type,
- ContentDisposition: `inline; filename="${ encodeURI(file.name) }"`,
}, (error) => {
if (error) {
diff --git a/app/file-upload/ufs/GoogleStorage/server.js b/app/file-upload/ufs/GoogleStorage/server.js
index b350caa6df38..47b384466549 100644
--- a/app/file-upload/ufs/GoogleStorage/server.js
+++ b/app/file-upload/ufs/GoogleStorage/server.js
@@ -30,10 +30,10 @@ export class GoogleStorageStore extends UploadFS.Store {
}
};
- this.getRedirectURL = function(file, callback) {
+ this.getRedirectURL = function(file, forceDownload = false, callback) {
const params = {
action: 'read',
- responseDisposition: 'inline',
+ responseDisposition: forceDownload ? 'attachment' : 'inline',
expires: Date.now() + this.options.URLExpiryTimeSpan * 1000,
};
diff --git a/app/google-vision/server/googlevision.js b/app/google-vision/server/googlevision.js
index 35717a623243..33494b398c1a 100644
--- a/app/google-vision/server/googlevision.js
+++ b/app/google-vision/server/googlevision.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { settings } from '../../settings';
import { callbacks } from '../../callbacks';
diff --git a/app/grant-facebook/README.md b/app/grant-facebook/README.md
deleted file mode 100644
index 9d2da06860b2..000000000000
--- a/app/grant-facebook/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# rocketchat:grant-facebook
-
-An implementation of the Facebook OAuth flow.
diff --git a/app/grant-facebook/index.js b/app/grant-facebook/index.js
deleted file mode 100644
index ca39cd0df4b1..000000000000
--- a/app/grant-facebook/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './server/index';
diff --git a/app/grant-facebook/server/index.js b/app/grant-facebook/server/index.js
deleted file mode 100644
index be3e0759abf7..000000000000
--- a/app/grant-facebook/server/index.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { HTTP } from 'meteor/http';
-
-import { Providers, GrantError } from '../../grant';
-
-const userAgent = 'Meteor';
-const version = 'v2.10';
-
-function getIdentity(accessToken, fields) {
- try {
- return HTTP.get(
- `https://graph.facebook.com/${ version }/me`, {
- headers: { 'User-Agent': userAgent },
- params: {
- access_token: accessToken,
- fields: fields.join(','),
- },
- }).data;
- } catch (err) {
- throw new GrantError(`Failed to fetch identity from Facebook. ${ err.message }`);
- }
-}
-
-function getPicture(accessToken) {
- try {
- return HTTP.get(
- `https://graph.facebook.com/${ version }/me/picture`, {
- headers: { 'User-Agent': userAgent },
- params: {
- redirect: false,
- height: 200,
- width: 200,
- type: 'normal',
- access_token: accessToken,
- },
- }).data;
- } catch (err) {
- throw new GrantError(`Failed to fetch profile picture from Facebook. ${ err.message }`);
- }
-}
-
-export function getUser(accessToken) {
- const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name'];
- const identity = getIdentity(accessToken, whitelisted);
- const avatar = getPicture(accessToken);
- const username = identity.name.toLowerCase().replace(' ', '.');
-
- return {
- id: identity.id,
- email: identity.email,
- username,
- name: `${ identity.first_name } ${ identity.last_name }`,
- avatar: avatar.data.url,
- };
-}
-
-// Register Facebook OAuth
-Providers.register('facebook', { scope: ['public_profile', 'email'] }, getUser);
diff --git a/app/grant-github/README.md b/app/grant-github/README.md
deleted file mode 100644
index e5fea3c781c7..000000000000
--- a/app/grant-github/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# rocketchat:grant-github
-
-An implementation of the GitHub OAuth flow.
diff --git a/app/grant-github/index.js b/app/grant-github/index.js
deleted file mode 100644
index ca39cd0df4b1..000000000000
--- a/app/grant-github/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './server/index';
diff --git a/app/grant-github/server/index.js b/app/grant-github/server/index.js
deleted file mode 100644
index e01741e304f6..000000000000
--- a/app/grant-github/server/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { HTTP } from 'meteor/http';
-
-import { Providers, GrantError } from '../../grant';
-
-const userAgent = 'Meteor';
-
-function getIdentity(accessToken) {
- try {
- return HTTP.get(
- 'https://api.github.com/user', {
- headers: { 'User-Agent': userAgent }, // http://developer.github.com/v3/#user-agent-required
- params: { access_token: accessToken },
- }).data;
- } catch (err) {
- throw new GrantError(`Failed to fetch identity from Github. ${ err.message }`);
- }
-}
-
-function getEmails(accessToken) {
- try {
- return HTTP.get(
- 'https://api.github.com/user/emails', {
- headers: { 'User-Agent': userAgent }, // http://developer.github.com/v3/#user-agent-required
- params: { access_token: accessToken },
- }).data;
- } catch (err) {
- return [];
- }
-}
-
-export function getUser(accessToken) {
- const identity = getIdentity(accessToken);
- const emails = getEmails(accessToken);
- const primaryEmail = (emails || []).find((email) => email.primary === true);
-
- return {
- id: identity.id,
- email: identity.email || (primaryEmail && primaryEmail.email) || '',
- username: identity.login,
- emails,
- name: identity.name,
- avatar: identity.avatar_url,
- };
-}
-
-// Register GitHub OAuth
-Providers.register('github', { scope: ['user', 'user:email'] }, getUser);
diff --git a/app/grant-google/README.md b/app/grant-google/README.md
deleted file mode 100644
index cdd59d97b562..000000000000
--- a/app/grant-google/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# rocketchat:grant-google
-
-An implementation of the Google OAuth flow.
diff --git a/app/grant-google/index.js b/app/grant-google/index.js
deleted file mode 100644
index ca39cd0df4b1..000000000000
--- a/app/grant-google/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './server/index';
diff --git a/app/grant-google/server/index.js b/app/grant-google/server/index.js
deleted file mode 100644
index 5c4aa9dad0da..000000000000
--- a/app/grant-google/server/index.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { HTTP } from 'meteor/http';
-
-import { Providers, GrantError } from '../../grant';
-
-const userAgent = 'Meteor';
-
-function getIdentity(accessToken) {
- try {
- return HTTP.get(
- 'https://www.googleapis.com/oauth2/v1/userinfo', {
- headers: { 'User-Agent': userAgent },
- params: {
- access_token: accessToken,
- },
- }).data;
- } catch (err) {
- throw new GrantError(`Failed to fetch identity from Google. ${ err.message }`);
- }
-}
-
-export function getUser(accessToken) {
- const whitelisted = [
- 'id', 'email', 'verified_email', 'name',
- 'given_name', 'family_name', 'picture',
- ];
- const identity = getIdentity(accessToken, whitelisted);
- const username = `${ identity.given_name.toLowerCase() }.${ identity.family_name.toLowerCase() }`;
-
- return {
- id: identity.id,
- email: identity.email,
- username,
- name: identity.name,
- avatar: identity.picture,
- };
-}
-
-// Register Google OAuth
-Providers.register('google', { scope: ['openid', 'email'] }, getUser);
diff --git a/app/grant/README.md b/app/grant/README.md
deleted file mode 100644
index 716b57bfcf80..000000000000
--- a/app/grant/README.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# rocketchat:grant
-
-The main idea behind creating this package was to allow external apps (i.e. PWA) to use OAuth smoothely with currently available accounts system.
-
-## Usage
-
-1. Define providers using `Settings.add()`
-1. Add apps with `Settings.apps.add()`
-1. Put the path that stars OAuth flow in your app
-1. You app should be able to authenticate user with received tokens
-
-## Paths
-
-There are few paths you need to be familiar with.
-
-### Start OAuth flow
-
-> \/_oauth_apps/connect/\/\
-
-### Authorization callback URL
-
-> \/_oauth_apps/connect/\/callback
-
-### List of available providers
-
-> \/_oauth_apps/providers
-
-## API
-
-### Providers
-
-#### Providers.register(name, options, getUser)
-
-Allows to register an OAuth Provider.
-
-- name - string that represents the name of an OAuth provider
-- options - contains fields like _scope_
-- getUser - a function that returns fields: _id, email, username, name and avatar_
-
-### Settings
-
-#### Settings.add(options)
-
-Defines a provider that is able for being used in OAuth.
-
-**options**:
-
-- enabled - __boolean__ - tells to `rocketchat:grant` if provider could be used
-- provider - __string__ - id of a provider
-- key - __string__ - client ID provided for your OAuth access
-- secret - __string__ - secret key
-
-Example:
-
-```js
- Settings.add({
- enabled: true,
- provider: 'google',
- key: 'CLIENT_ID',
- secret: 'SECRET'
- });
-```
-
-#### Settings.apps.add(name, options)
-
-Defines an app that is able for using OAuth.
-
-**options**:
-
-- redirectUrl - __string__ - where to redirect if auth was succesful
-- errorUrl - __string__ - place to redirect on failure
-
-Example:
-
-```js
-
- const redirectUrl = 'http://localhost:4200/login?service={provider}&access_token={accessToken}&refresh_token={refreshToken}';
-
- const errorUrl = 'http://localhost:4200/login?service={provider}&error={error}'
-
-
- Settings.apps.add('PWA', {
- redirectUrl,
- errorUrl
- });
-```
-
-About URLs:
-
-We use a parser to produce a URL.
-There are few available variables for each type of redirect.
-
-- redirectUrl - provider, accessToken, refreshToken
-- errorUrl - provider, error
-
-Example:
-
-```
-http://localhost:4200/login?provider={provider}
-// outputs: http://localhost:4200/login?provider=google
-```
diff --git a/app/grant/index.js b/app/grant/index.js
deleted file mode 100644
index ca39cd0df4b1..000000000000
--- a/app/grant/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './server/index';
diff --git a/app/grant/server/authenticate.js b/app/grant/server/authenticate.js
deleted file mode 100644
index 48f7b0976200..000000000000
--- a/app/grant/server/authenticate.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import { Accounts } from 'meteor/accounts-base';
-import { Meteor } from 'meteor/meteor';
-
-import { GrantError } from './error';
-import Providers from './providers';
-import { AccountsServer } from '../../accounts';
-import { Users } from '../../models';
-import { t } from '../../utils';
-
-const setAvatarFromUrl = (userId, url) => new Promise((resolve, reject) => {
- Meteor.runAsUser(userId, () => {
- Meteor.call('setAvatarFromService', url, '', 'url', (err) => {
- if (err) {
- if (err.details && err.details.timeToReset) {
- reject(t('error-too-many-requests', {
- seconds: parseInt(err.details.timeToReset / 1000),
- }));
- } else {
- reject(t('Avatar_url_invalid_or_error'));
- }
- } else {
- resolve();
- }
- });
- });
-});
-
-const findUserByOAuthId = (providerName, id) => Users.findOne({ [`settings.profile.oauth.${ providerName }`]: id });
-
-const addOAuthIdToUserProfile = (user, providerName, providerId) => {
- const profile = Object.assign({}, user.settings.profile, {
- oauth: {
- ...user.settings.profile.oauth,
- [providerName]: providerId,
- },
- });
-
- Users.setProfile(user.id, profile);
-};
-
-function getAccessToken(req) {
- const i = req.url.indexOf('?');
-
- if (i === -1) {
- return;
- }
-
- const barePath = req.url.substring(i + 1);
- const splitPath = barePath.split('&');
- const token = splitPath.find((p) => p.match(/access_token=[a-zA-Z0-9]+/));
-
- if (token) {
- return token.replace('access_token=', '');
- }
-}
-
-export async function authenticate(providerName, req) {
- let tokens;
- const accessToken = getAccessToken(req);
- const provider = Providers.get(providerName);
-
- if (!provider) {
- throw new GrantError(`Provider '${ providerName }' not found`);
- }
-
- const userData = provider.getUser(accessToken);
-
- let user = findUserByOAuthId(providerName, userData.id);
-
- if (user) {
- user.id = user._id;
- } else {
- user = Users.findOneByEmailAddress(userData.email);
- if (user) {
- user.id = user._id;
- }
- }
-
- if (user) {
- addOAuthIdToUserProfile(user, providerName, userData.id);
-
- const loginResult = await AccountsServer.loginWithUser({ id: user.id });
-
- tokens = loginResult.tokens;
- } else {
- const id = Accounts.createUser({
- email: userData.email,
- username: userData.username,
- });
-
- Users.setProfile(id, {
- avatar: userData.avatar,
- oauth: {
- [providerName]: userData.id,
- },
- });
- Users.setName(id, userData.name);
- Users.setEmailVerified(id, userData.email);
-
- await setAvatarFromUrl(id, userData.avatar);
-
- const loginResult = await AccountsServer.loginWithUser({ id });
-
- tokens = loginResult.tokens;
- }
-
- return tokens;
-}
diff --git a/app/grant/server/error.js b/app/grant/server/error.js
deleted file mode 100644
index 249319e6b912..000000000000
--- a/app/grant/server/error.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export class GrantError extends Error {
-}
diff --git a/app/grant/server/grant.js b/app/grant/server/grant.js
deleted file mode 100644
index b6a256a32e31..000000000000
--- a/app/grant/server/grant.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import Providers from './providers';
-import Settings from './settings';
-import { path, generateCallback, generateAppCallback } from './routes';
-import { hostname } from '../../lib';
-
-function addProviders(config) {
- Settings.forEach((settings, providerName) => {
- if (settings.enabled === true) {
- const registeredProvider = Providers.get(providerName);
-
- if (!registeredProvider) {
- console.error(`No configuration for '${ providerName }' provider`);
- }
-
- // basic settings
- const data = {
- key: settings.key,
- secret: settings.secret,
- scope: registeredProvider.scope,
- callback: generateCallback(providerName),
- };
-
- // set each app
- Settings.apps.forEach((_, appName) => {
- data[appName] = {
- callback: generateAppCallback(providerName, appName),
- };
- });
-
- config[providerName] = data;
- }
- });
-}
-
-const config = {};
-
-export function generateConfig() {
- config.server = {
- protocol: 'http',
- host: hostname,
- path,
- state: true,
- };
-
- addProviders(config);
-
- return config;
-}
-
-export function getConfig() {
- return config;
-}
diff --git a/app/grant/server/index.js b/app/grant/server/index.js
deleted file mode 100644
index 8b07f8ece918..000000000000
--- a/app/grant/server/index.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { WebApp } from 'meteor/webapp';
-import session from 'express-session';
-import Grant from 'grant-express';
-import fiber from 'fibers';
-
-import { GrantError } from './error';
-import { generateConfig } from './grant';
-import { path, generateCallback, generateAppCallback } from './routes';
-import { middleware as redirect } from './redirect';
-import Providers, { middleware as providers } from './providers';
-import Settings from './settings';
-
-let grant;
-
-WebApp.connectHandlers.use(session({
- secret: 'grant',
- resave: true,
- saveUninitialized: true,
-}));
-
-// grant
-WebApp.connectHandlers.use(path, (req, res, next) => {
- if (grant) {
- grant(req, res, next);
- } else {
- next();
- }
-});
-
-// callbacks
-WebApp.connectHandlers.use((req, res, next) => {
- fiber(() => {
- redirect(req, res, next);
- }).run();
-});
-
-// providers
-WebApp.connectHandlers.use((req, res, next) => {
- fiber(() => {
- providers(req, res, next);
- }).run();
-});
-
-Meteor.startup(() => {
- const config = generateConfig();
-
- grant = new Grant(config);
-});
-
-export {
- path,
- generateCallback,
- generateAppCallback,
- Providers,
- Settings,
- GrantError,
-};
diff --git a/app/grant/server/providers.js b/app/grant/server/providers.js
deleted file mode 100644
index 59735448e466..000000000000
--- a/app/grant/server/providers.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Match, check } from 'meteor/check';
-
-import { Storage } from './storage';
-import { routes } from './routes';
-
-class Providers extends Storage {
- register(name, options, getUser) {
- check(name, String);
- check(options, {
- scope: Match.OneOf(String, [String]),
- });
- check(getUser, Function);
-
- this._add(name.toLowerCase(), {
- scope: options.scope,
- getUser,
- });
- }
-}
-
-const providers = new Providers();
-
-export default providers;
-
-export function middleware(req, res, next) {
- const route = routes.providers(req);
-
- if (route) {
- const list = [];
-
- providers.forEach((_, name) => list.push(name));
-
- res.end(JSON.stringify({
- data: list,
- }));
- return;
- }
-
- next();
-}
diff --git a/app/grant/server/redirect.js b/app/grant/server/redirect.js
deleted file mode 100644
index 3cb5ff071fcc..000000000000
--- a/app/grant/server/redirect.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { authenticate } from './authenticate';
-import Settings from './settings';
-import { routes } from './routes';
-import { GrantError } from './error';
-
-function parseUrl(url, config) {
- return url.replace(/\{[\ ]*(provider|accessToken|refreshToken|error)[\ ]*\}/g, (_, key) => config[key]);
-}
-
-function getAppConfig(providerName, appName) {
- const providerConfig = Settings.get(providerName);
-
- if (providerConfig) {
- return Settings.apps.get(appName);
- }
-}
-
-export async function middleware(req, res, next) {
- const route = routes.appCallback(req);
-
- // handle app callback
- if (route) {
- const config = {
- provider: route.provider,
- };
- const appConfig = getAppConfig(route.provider, route.app);
-
- if (appConfig) {
- const {
- redirectUrl,
- errorUrl,
- } = appConfig;
-
- try {
- const tokens = await authenticate(route.provider, req);
-
- config.accessToken = tokens.accessToken;
- config.refreshToken = tokens.refreshToken;
-
- res.redirect(parseUrl(redirectUrl, config));
- return;
- } catch (error) {
- config.error = error instanceof GrantError ? error.message : 'Something went wrong';
-
- console.error(error);
-
- res.redirect(parseUrl(errorUrl, config));
- return;
- }
- }
- }
-
- next();
-}
diff --git a/app/grant/server/routes.js b/app/grant/server/routes.js
deleted file mode 100644
index b65e0e6e25d4..000000000000
--- a/app/grant/server/routes.js
+++ /dev/null
@@ -1,48 +0,0 @@
-export const path = '/_oauth_apps';
-
-export function generateCallback(providerName) {
- return `${ path }/${ providerName }/callback`;
-}
-
-export function generateAppCallback(providerName, appName) {
- return generateCallback(`${ providerName }/${ appName }`);
-}
-
-export function getPaths(req) {
- const i = req.url.indexOf('?');
- let barePath;
-
- if (i === -1) {
- barePath = req.url;
- } else {
- barePath = req.url.substring(0, i);
- }
-
- const splitPath = barePath.split('/');
-
- // Any non-oauth request will continue down the default
- // middlewares.
- if (splitPath[1] === '_oauth_apps') {
- return splitPath.slice(2);
- }
-}
-
-export const routes = {
- // :path/:provider/:app/callback
- appCallback: (req) => {
- const paths = getPaths(req);
-
- if (paths && paths[2] === 'callback') {
- return {
- provider: paths[0],
- app: paths[1],
- };
- }
- },
- // :path/providers
- providers: (req) => {
- const paths = getPaths(req);
-
- return paths && paths[0] === 'providers';
- },
-};
diff --git a/app/grant/server/settings.js b/app/grant/server/settings.js
deleted file mode 100644
index c5145b5649e3..000000000000
--- a/app/grant/server/settings.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Match, check } from 'meteor/check';
-
-import { Storage } from './storage';
-
-class Apps extends Storage {
- add(name, body) {
- check(name, String);
- check(body, {
- redirectUrl: String,
- errorUrl: String,
- });
-
- this._add(name, body);
- }
-}
-
-class Settings extends Storage {
- constructor() {
- super();
-
- this.apps = new Apps();
- }
-
- add(settings) {
- check(settings, {
- enabled: Match.Optional(Boolean),
- provider: String,
- key: String,
- secret: String,
- });
-
- this._add(settings.provider, {
- enabled: settings.enabled === true,
- provider: settings.provider,
- key: settings.key,
- secret: settings.secret,
- });
- }
-}
-
-const settings = new Settings();
-
-export default settings;
diff --git a/app/grant/server/storage.js b/app/grant/server/storage.js
deleted file mode 100644
index 90d506681a83..000000000000
--- a/app/grant/server/storage.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export class Storage {
- constructor() {
- this._data = {};
- }
-
- all() {
- return this._data;
- }
-
- forEach(fn) {
- Object.keys(this.all())
- .forEach((name) => {
- fn(this.get(name), name);
- });
- }
-
- get(name) {
- return this.all()[name.toLowerCase()];
- }
-
- has(name) {
- return !!this._data[name];
- }
-
- _add(name, body) {
- if (this.has(name)) {
- console.error(`'${ name }' have been already defined`);
- return;
- }
-
- this._data[name] = body;
- }
-}
diff --git a/app/graphql/README.md b/app/graphql/README.md
deleted file mode 100644
index 712c36fd39ae..000000000000
--- a/app/graphql/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# rocketchat:graphql
-
-GraphQL API
diff --git a/app/graphql/index.js b/app/graphql/index.js
deleted file mode 100644
index ca39cd0df4b1..000000000000
--- a/app/graphql/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './server/index';
diff --git a/app/graphql/server/api.js b/app/graphql/server/api.js
deleted file mode 100644
index 60e56262e109..000000000000
--- a/app/graphql/server/api.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
-import { JSAccountsContext as jsAccountsContext } from '@accounts/graphql-api';
-import { SubscriptionServer } from 'subscriptions-transport-ws';
-import { execute, subscribe } from 'graphql';
-import { Meteor } from 'meteor/meteor';
-import { WebApp } from 'meteor/webapp';
-import bodyParser from 'body-parser';
-import express from 'express';
-import cors from 'cors';
-
-import { executableSchema } from './schema';
-import { settings } from '../../settings';
-
-
-const subscriptionPort = settings.get('Graphql_Subscription_Port') || 3100;
-
-// the Meteor GraphQL server is an Express server
-const graphQLServer = express();
-
-graphQLServer.disable('x-powered-by');
-
-if (settings.get('Graphql_CORS')) {
- graphQLServer.use(cors());
-}
-
-graphQLServer.use('/api/graphql', (req, res, next) => {
- if (settings.get('Graphql_Enabled')) {
- next();
- } else {
- res.status(400).send('Graphql is not enabled in this server');
- }
-});
-
-graphQLServer.use(
- '/api/graphql',
- bodyParser.json(),
- graphqlExpress((request) => ({
- schema: executableSchema,
- context: jsAccountsContext(request),
- formatError: (e) => ({
- message: e.message,
- locations: e.locations,
- path: e.path,
- }),
- debug: Meteor.isDevelopment,
- }))
-);
-
-graphQLServer.use(
- '/graphiql',
- graphiqlExpress({
- endpointURL: '/api/graphql',
- subscriptionsEndpoint: `ws://localhost:${ subscriptionPort }`,
- })
-);
-
-const startSubscriptionServer = () => {
- if (settings.get('Graphql_Enabled')) {
- SubscriptionServer.create({
- schema: executableSchema,
- execute,
- subscribe,
- onConnect: (connectionParams) => ({ authToken: connectionParams.Authorization }),
- },
- {
- port: subscriptionPort,
- host: process.env.BIND_IP || '0.0.0.0',
- });
-
- console.log('GraphQL Subscription server runs on port:', subscriptionPort);
- }
-};
-
-WebApp.onListening(() => {
- startSubscriptionServer();
-});
-
-// this binds the specified paths to the Express server running Apollo + GraphiQL
-WebApp.connectHandlers.use(graphQLServer);
diff --git a/app/graphql/server/helpers/authenticated.js b/app/graphql/server/helpers/authenticated.js
deleted file mode 100644
index 4e542109459c..000000000000
--- a/app/graphql/server/helpers/authenticated.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { AccountsServer } from '../../../accounts';
-// import { authenticated as _authenticated } from '@accounts/graphql-api';
-import { authenticated as _authenticated } from '../mocks/accounts/graphql-api';
-
-export const authenticated = (resolver) => _authenticated(AccountsServer, resolver);
diff --git a/app/graphql/server/helpers/dateToFloat.js b/app/graphql/server/helpers/dateToFloat.js
deleted file mode 100644
index faf206706f1b..000000000000
--- a/app/graphql/server/helpers/dateToFloat.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export function dateToFloat(date) {
- if (date) {
- return new Date(date).getTime();
- }
-}
diff --git a/app/graphql/server/index.js b/app/graphql/server/index.js
deleted file mode 100644
index c19d98f29eb5..000000000000
--- a/app/graphql/server/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './settings';
-import './api';
diff --git a/app/graphql/server/mocks/accounts/graphql-api.js b/app/graphql/server/mocks/accounts/graphql-api.js
deleted file mode 100644
index 4dc6a6783eca..000000000000
--- a/app/graphql/server/mocks/accounts/graphql-api.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Same as here: https://github.com/js-accounts/graphql/blob/master/packages/graphql-api/src/utils/authenticated-resolver.js
-// except code below works
-// It might be like that because of async/await,
-// maybe Promise is not wrapped with Fiber
-// See: https://github.com/meteor/meteor/blob/a362e20a37547362b581fed52f7171d022e83b62/packages/promise/server.js
-// Opened issue: https://github.com/js-accounts/graphql/issues/16
-export const authenticated = (Accounts, func) => async (root, args, context, info) => {
- const { authToken } = context;
-
- if (!authToken || authToken === '' || authToken === null) {
- throw new Error('Unable to find authorization token in request');
- }
-
- const userObject = await Accounts.resumeSession(authToken);
-
- if (userObject === null) {
- throw new Error('Invalid or expired token!');
- }
-
- return func(root, args, Object.assign(context, { user: userObject }), info);
-};
diff --git a/app/graphql/server/resolvers/accounts/OauthProvider-type.js b/app/graphql/server/resolvers/accounts/OauthProvider-type.js
deleted file mode 100644
index 0444479b4bf3..000000000000
--- a/app/graphql/server/resolvers/accounts/OauthProvider-type.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/accounts/OauthProvider-type.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/accounts/index.js b/app/graphql/server/resolvers/accounts/index.js
deleted file mode 100644
index 8d49638291a7..000000000000
--- a/app/graphql/server/resolvers/accounts/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { createJSAccountsGraphQL } from '@accounts/graphql-api';
-import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
-
-
-// queries
-import * as oauthProviders from './oauthProviders';
-// types
-import * as OauthProviderType from './OauthProvider-type';
-import { AccountsServer } from '../../../../accounts';
-
-const accountsGraphQL = createJSAccountsGraphQL(AccountsServer);
-
-export const schema = mergeTypes([
- accountsGraphQL.schema,
- oauthProviders.schema,
- OauthProviderType.schema,
-]);
-
-export const resolvers = mergeResolvers([
- accountsGraphQL.extendWithResolvers({}),
- oauthProviders.resolver,
-]);
diff --git a/app/graphql/server/resolvers/accounts/oauthProviders.js b/app/graphql/server/resolvers/accounts/oauthProviders.js
deleted file mode 100644
index 7928e07705e3..000000000000
--- a/app/graphql/server/resolvers/accounts/oauthProviders.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { HTTP } from 'meteor/http';
-import { Meteor } from 'meteor/meteor';
-
-import schema from '../../schemas/accounts/oauthProviders.graphqls';
-
-function isJSON(obj) {
- try {
- JSON.parse(obj);
- return true;
- } catch (e) {
- return false;
- }
-}
-
-const resolver = {
- Query: {
- oauthProviders: async () => {
- // depends on rocketchat:grant package
- try {
- const result = HTTP.get(Meteor.absoluteUrl('_oauth_apps/providers')).content;
-
- if (isJSON(result)) {
- const providers = JSON.parse(result).data;
-
- return providers.map((name) => ({ name }));
- }
- throw new Error('Could not parse the result');
- } catch (e) {
- throw new Error('rocketchat:grant not installed');
- }
- },
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/Channel-type.js b/app/graphql/server/resolvers/channels/Channel-type.js
deleted file mode 100644
index c21d7102ca84..000000000000
--- a/app/graphql/server/resolvers/channels/Channel-type.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import property from 'lodash.property';
-
-import { Subscriptions, Users } from '../../../../models';
-import schema from '../../schemas/channels/Channel-type.graphqls';
-
-const resolver = {
- Channel: {
- id: property('_id'),
- name: (root, args, { user }) => {
- if (root.t === 'd') {
- return root.usernames.find((u) => u !== user.username);
- }
-
- return root.name;
- },
- members: (root) => {
- const ids = Subscriptions.findByRoomIdWhenUserIdExists(root._id, { fields: { 'u._id': 1 } })
- .fetch()
- .map((sub) => sub.u._id);
- return Users.findByIds(ids).fetch();
- },
- owners: (root) => {
- // there might be no owner
- if (!root.u) {
- return;
- }
-
- return [Users.findOneByUsername(root.u.username)];
- },
- numberOfMembers: (root) => Subscriptions.findByRoomId(root._id).count(),
- numberOfMessages: property('msgs'),
- readOnly: (root) => root.ro === true,
- direct: (root) => root.t === 'd',
- privateChannel: (root) => root.t === 'p',
- favourite: (root, args, { user }) => {
- const room = Subscriptions.findOneByRoomIdAndUserId(root._id, user._id);
-
- return room && room.f === true;
- },
- unseenMessages: (root, args, { user }) => {
- const room = Subscriptions.findOneByRoomIdAndUserId(root._id, user._id);
-
- return (room || {}).unread;
- },
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/ChannelFilter-input.js b/app/graphql/server/resolvers/channels/ChannelFilter-input.js
deleted file mode 100644
index c34d51653561..000000000000
--- a/app/graphql/server/resolvers/channels/ChannelFilter-input.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/channels/ChannelFilter-input.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/channels/ChannelNameAndDirect-input.js b/app/graphql/server/resolvers/channels/ChannelNameAndDirect-input.js
deleted file mode 100644
index 5285a9ac0fc0..000000000000
--- a/app/graphql/server/resolvers/channels/ChannelNameAndDirect-input.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/channels/ChannelNameAndDirect-input.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/channels/ChannelSort-enum.js b/app/graphql/server/resolvers/channels/ChannelSort-enum.js
deleted file mode 100644
index fbd6ec864362..000000000000
--- a/app/graphql/server/resolvers/channels/ChannelSort-enum.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/channels/ChannelSort-enum.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/channels/Privacy-enum.js b/app/graphql/server/resolvers/channels/Privacy-enum.js
deleted file mode 100644
index b306754f6075..000000000000
--- a/app/graphql/server/resolvers/channels/Privacy-enum.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/channels/Privacy-enum.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/channels/channelByName.js b/app/graphql/server/resolvers/channels/channelByName.js
deleted file mode 100644
index 21fb67f95155..000000000000
--- a/app/graphql/server/resolvers/channels/channelByName.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { roomPublicFields } from './settings';
-import { Rooms } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/channelByName.graphqls';
-
-const resolver = {
- Query: {
- channelByName: authenticated((root, { name }) => {
- const query = {
- name,
- t: 'c',
- };
-
- return Rooms.findOne(query, {
- fields: roomPublicFields,
- });
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/channels.js b/app/graphql/server/resolvers/channels/channels.js
deleted file mode 100644
index 638504a3b5e7..000000000000
--- a/app/graphql/server/resolvers/channels/channels.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { roomPublicFields } from './settings';
-import { Rooms } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/channels.graphqls';
-
-const resolver = {
- Query: {
- channels: authenticated((root, args) => {
- const query = {};
- const options = {
- sort: {
- name: 1,
- },
- fields: roomPublicFields,
- };
-
- // Filter
- if (typeof args.filter !== 'undefined') {
- // nameFilter
- if (typeof args.filter.nameFilter !== 'undefined') {
- query.name = {
- $regex: new RegExp(args.filter.nameFilter, 'i'),
- };
- }
-
- // sortBy
- if (args.filter.sortBy === 'NUMBER_OF_MESSAGES') {
- options.sort = {
- msgs: -1,
- };
- }
-
- // privacy
- switch (args.filter.privacy) {
- case 'PRIVATE':
- query.t = 'p';
- break;
- case 'PUBLIC':
- query.t = {
- $ne: 'p',
- };
- break;
- }
- }
-
- return Rooms.find(query, options).fetch();
- }),
- },
-};
-
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/channelsByUser.js b/app/graphql/server/resolvers/channels/channelsByUser.js
deleted file mode 100644
index 09cff6675773..000000000000
--- a/app/graphql/server/resolvers/channels/channelsByUser.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { roomPublicFields } from './settings';
-import { Users, Subscriptions, Rooms } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/channelsByUser.graphqls';
-
-const resolver = {
- Query: {
- channelsByUser: authenticated((root, { userId }) => {
- const user = Users.findOneById(userId);
-
- if (!user) {
- throw new Error('No user');
- }
-
- const roomIds = Subscriptions.findByUserId(userId, { fields: { rid: 1 } }).fetch().map((s) => s.rid);
- const rooms = Rooms.findByIds(roomIds, {
- sort: {
- name: 1,
- },
- fields: roomPublicFields,
- }).fetch();
-
- return rooms;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/createChannel.js b/app/graphql/server/resolvers/channels/createChannel.js
deleted file mode 100644
index 3ef544bb1994..000000000000
--- a/app/graphql/server/resolvers/channels/createChannel.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { API } from '../../../../api';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/createChannel.graphqls';
-
-const resolver = {
- Mutation: {
- createChannel: authenticated((root, args, { user }) => {
- try {
- API.channels.create.validate({
- user: {
- value: user._id,
- },
- name: {
- value: args.name,
- key: 'name',
- },
- members: {
- value: args.membersId,
- key: 'membersId',
- },
- });
- } catch (e) {
- throw e;
- }
-
- const { channel } = API.channels.create.execute(user._id, {
- name: args.name,
- members: args.membersId,
- });
-
- return channel;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/deleteChannel.js b/app/graphql/server/resolvers/channels/deleteChannel.js
deleted file mode 100644
index 3c3aaca6de96..000000000000
--- a/app/graphql/server/resolvers/channels/deleteChannel.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Rooms, Subscriptions } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/deleteChannel.graphqls';
-
-const resolver = {
- Mutation: {
- deleteChannel: authenticated((root, { channelId }, { user }) => {
- const channel = Rooms.findOne({
- _id: channelId,
- t: 'c',
- });
-
- if (!channel) {
- throw new Error('error-room-not-found', 'The required "channelId" param provided does not match any channel');
- }
-
- const sub = Subscriptions.findOneByRoomIdAndUserId(channel._id, user._id);
-
- if (!sub) {
- throw new Error(`The user/callee is not in the channel "${ channel.name }.`);
- }
-
- if (!sub.open) {
- throw new Error(`The channel, ${ channel.name }, is already closed to the sender`);
- }
-
- Meteor.runAsUser(user._id, () => {
- Meteor.call('eraseRoom', channel._id);
- });
-
- return true;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/directChannel.js b/app/graphql/server/resolvers/channels/directChannel.js
deleted file mode 100644
index a23219814f17..000000000000
--- a/app/graphql/server/resolvers/channels/directChannel.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { roomPublicFields } from './settings';
-import { Rooms } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/directChannel.graphqls';
-
-const resolver = {
- Query: {
- directChannel: authenticated((root, { username, channelId }, { user }) => {
- const query = {
- t: 'd',
- usernames: user.username,
- };
-
- if (typeof username !== 'undefined') {
- if (username === user.username) {
- throw new Error('You cannot specify your username');
- }
-
- query.usernames = { $all: [user.username, username] };
- } else if (typeof channelId !== 'undefined') {
- query.id = channelId;
- } else {
- throw new Error('Use one of those fields: username, channelId');
- }
-
- return Rooms.findOne(query, {
- fields: roomPublicFields,
- });
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/hideChannel.js b/app/graphql/server/resolvers/channels/hideChannel.js
deleted file mode 100644
index b0865c7e4fb1..000000000000
--- a/app/graphql/server/resolvers/channels/hideChannel.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Rooms, Subscriptions } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/hideChannel.graphqls';
-
-const resolver = {
- Mutation: {
- hideChannel: authenticated((root, args, { user }) => {
- const channel = Rooms.findOne({
- _id: args.channelId,
- t: 'c',
- });
-
- if (!channel) {
- throw new Error('error-room-not-found', 'The required "channelId" param provided does not match any channel');
- }
-
- const sub = Subscriptions.findOneByRoomIdAndUserId(channel._id, user._id);
-
- if (!sub) {
- throw new Error(`The user/callee is not in the channel "${ channel.name }.`);
- }
-
- if (!sub.open) {
- throw new Error(`The channel, ${ channel.name }, is already closed to the sender`);
- }
-
- Meteor.runAsUser(user._id, () => {
- Meteor.call('hideRoom', channel._id);
- });
-
- return true;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/index.js b/app/graphql/server/resolvers/channels/index.js
deleted file mode 100644
index f463dccabea4..000000000000
--- a/app/graphql/server/resolvers/channels/index.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
-
-// queries
-import * as channels from './channels';
-import * as channelByName from './channelByName';
-import * as directChannel from './directChannel';
-import * as channelsByUser from './channelsByUser';
-// mutations
-import * as createChannel from './createChannel';
-import * as leaveChannel from './leaveChannel';
-import * as hideChannel from './hideChannel';
-import * as deleteChannel from './deleteChannel';
-// types
-import * as ChannelType from './Channel-type';
-import * as ChannelSort from './ChannelSort-enum';
-import * as ChannelFilter from './ChannelFilter-input';
-import * as Privacy from './Privacy-enum';
-import * as ChannelNameAndDirect from './ChannelNameAndDirect-input';
-
-export const schema = mergeTypes([
- // queries
- channels.schema,
- channelByName.schema,
- directChannel.schema,
- channelsByUser.schema,
- // mutations
- createChannel.schema,
- leaveChannel.schema,
- hideChannel.schema,
- deleteChannel.schema,
- // types
- ChannelType.schema,
- ChannelSort.schema,
- ChannelFilter.schema,
- Privacy.schema,
- ChannelNameAndDirect.schema,
-]);
-
-export const resolvers = mergeResolvers([
- // queries
- channels.resolver,
- channelByName.resolver,
- directChannel.resolver,
- channelsByUser.resolver,
- // mutations
- createChannel.resolver,
- leaveChannel.resolver,
- hideChannel.resolver,
- deleteChannel.resolver,
- // types
- ChannelType.resolver,
-]);
diff --git a/app/graphql/server/resolvers/channels/leaveChannel.js b/app/graphql/server/resolvers/channels/leaveChannel.js
deleted file mode 100644
index 8448e778b539..000000000000
--- a/app/graphql/server/resolvers/channels/leaveChannel.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Rooms } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/channels/leaveChannel.graphqls';
-
-const resolver = {
- Mutation: {
- leaveChannel: authenticated((root, args, { user }) => {
- const channel = Rooms.findOne({
- _id: args.channelId,
- t: 'c',
- });
-
- if (!channel) {
- throw new Error('error-room-not-found', 'The required "channelId" param provided does not match any channel');
- }
-
- Meteor.runAsUser(user._id, () => {
- Meteor.call('leaveRoom', channel._id);
- });
-
- return true;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/channels/settings.js b/app/graphql/server/resolvers/channels/settings.js
deleted file mode 100644
index 4aa8081e1ebb..000000000000
--- a/app/graphql/server/resolvers/channels/settings.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export const roomPublicFields = {
- t: 1,
- name: 1,
- description: 1,
- announcement: 1,
- topic: 1,
- usernames: 1,
- msgs: 1,
- ro: 1,
- u: 1,
- archived: 1,
-};
diff --git a/app/graphql/server/resolvers/messages/Message-type.js b/app/graphql/server/resolvers/messages/Message-type.js
deleted file mode 100644
index 5f9bd1cbc69f..000000000000
--- a/app/graphql/server/resolvers/messages/Message-type.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import property from 'lodash.property';
-
-import { Rooms, Users } from '../../../../models';
-import { dateToFloat } from '../../helpers/dateToFloat';
-import schema from '../../schemas/messages/Message-type.graphqls';
-
-const resolver = {
- Message: {
- id: property('_id'),
- content: property('msg'),
- creationTime: (root) => dateToFloat(root.ts),
- author: (root) => {
- const user = Users.findOne(root.u._id);
-
- return user || root.u;
- },
- channel: (root) => Rooms.findOne(root.rid),
- fromServer: (root) => typeof root.t !== 'undefined', // on a message sent by user `true` otherwise `false`
- type: property('t'),
- channelRef: (root) => {
- if (!root.channels) {
- return;
- }
-
- return Rooms.find({
- _id: {
- $in: root.channels.map((c) => c._id),
- },
- }, {
- sort: {
- name: 1,
- },
- }).fetch();
- },
- userRef: (root) => {
- if (!root.mentions) {
- return;
- }
-
- return Users.find({
- _id: {
- $in: root.mentions.map((c) => c._id),
- },
- }, {
- sort: {
- username: 1,
- },
- }).fetch();
- },
- reactions: (root) => {
- if (!root.reactions || Object.keys(root.reactions).length === 0) {
- return;
- }
-
- const reactions = [];
-
- Object.keys(root.reactions).forEach((icon) => {
- root.reactions[icon].usernames.forEach((username) => {
- reactions.push({
- icon,
- username,
- });
- });
- });
-
- return reactions;
- },
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/messages/MessageIdentifier-input.js b/app/graphql/server/resolvers/messages/MessageIdentifier-input.js
deleted file mode 100644
index 010001040b60..000000000000
--- a/app/graphql/server/resolvers/messages/MessageIdentifier-input.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/messages/MessageIdentifier-input.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/messages/MessagesWithCursor-type.js b/app/graphql/server/resolvers/messages/MessagesWithCursor-type.js
deleted file mode 100644
index d6a720856b6a..000000000000
--- a/app/graphql/server/resolvers/messages/MessagesWithCursor-type.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/messages/MessagesWithCursor-type.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/messages/Reaction-type.js b/app/graphql/server/resolvers/messages/Reaction-type.js
deleted file mode 100644
index 843a7a940cd5..000000000000
--- a/app/graphql/server/resolvers/messages/Reaction-type.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/messages/Reaction-type.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/messages/addReactionToMessage.js b/app/graphql/server/resolvers/messages/addReactionToMessage.js
deleted file mode 100644
index a90b8e57f493..000000000000
--- a/app/graphql/server/resolvers/messages/addReactionToMessage.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Messages } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/messages/addReactionToMessage.graphqls';
-
-const resolver = {
- Mutation: {
- addReactionToMessage: authenticated((root, { id, icon, shouldReact }, { user }) => new Promise((resolve) => {
- Meteor.runAsUser(user._id, () => {
- Meteor.call('setReaction', icon, id.messageId, shouldReact, () => {
- resolve(Messages.findOne(id.messageId));
- });
- });
- })),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/messages/chatMessageAdded.js b/app/graphql/server/resolvers/messages/chatMessageAdded.js
deleted file mode 100644
index dc8ee0ddba7c..000000000000
--- a/app/graphql/server/resolvers/messages/chatMessageAdded.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { withFilter } from 'graphql-subscriptions';
-
-import { Rooms } from '../../../../models';
-import { callbacks } from '../../../../callbacks';
-import { pubsub } from '../../subscriptions';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/messages/chatMessageAdded.graphqls';
-
-export const CHAT_MESSAGE_SUBSCRIPTION_TOPIC = 'CHAT_MESSAGE_ADDED';
-
-export function publishMessage(message) {
- pubsub.publish(CHAT_MESSAGE_SUBSCRIPTION_TOPIC, { chatMessageAdded: message });
-}
-
-function shouldPublish(message, { id, directTo }, username) {
- if (id) {
- return message.rid === id;
- } if (directTo) {
- const room = Rooms.findOne({
- usernames: { $all: [directTo, username] },
- t: 'd',
- });
-
- return room && room._id === message.rid;
- }
-
- return false;
-}
-
-const resolver = {
- Subscription: {
- chatMessageAdded: {
- subscribe: withFilter(() => pubsub.asyncIterator(CHAT_MESSAGE_SUBSCRIPTION_TOPIC), authenticated((payload, args, { user }) => {
- const channel = {
- id: args.channelId,
- directTo: args.directTo,
- };
-
- return shouldPublish(payload.chatMessageAdded, channel, user.username);
- })),
- },
- },
-};
-
-callbacks.add('afterSaveMessage', (message) => {
- publishMessage(message);
-}, callbacks.priority.MEDIUM, 'chatMessageAddedSubscription');
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/messages/deleteMessage.js b/app/graphql/server/resolvers/messages/deleteMessage.js
deleted file mode 100644
index feb2374ba29c..000000000000
--- a/app/graphql/server/resolvers/messages/deleteMessage.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Messages } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/messages/deleteMessage.graphqls';
-
-const resolver = {
- Mutation: {
- deleteMessage: authenticated((root, { id }, { user }) => {
- const msg = Messages.findOneById(id.messageId, { fields: { u: 1, rid: 1 } });
-
- if (!msg) {
- throw new Error(`No message found with the id of "${ id.messageId }".`);
- }
-
- if (id.channelId !== msg.rid) {
- throw new Error('The room id provided does not match where the message is from.');
- }
-
- Meteor.runAsUser(user._id, () => {
- Meteor.call('deleteMessage', { _id: msg._id });
- });
-
- return msg;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/messages/editMessage.js b/app/graphql/server/resolvers/messages/editMessage.js
deleted file mode 100644
index 9511a3dfc103..000000000000
--- a/app/graphql/server/resolvers/messages/editMessage.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-
-import { Messages } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/messages/editMessage.graphqls';
-
-const resolver = {
- Mutation: {
- editMessage: authenticated((root, { id, content }, { user }) => {
- const msg = Messages.findOneById(id.messageId);
-
- // Ensure the message exists
- if (!msg) {
- throw new Error(`No message found with the id of "${ id.messageId }".`);
- }
-
- if (id.channelId !== msg.rid) {
- throw new Error('The channel id provided does not match where the message is from.');
- }
-
- // Permission checks are already done in the updateMessage method, so no need to duplicate them
- Meteor.runAsUser(user._id, () => {
- Meteor.call('updateMessage', { _id: msg._id, msg: content, rid: msg.rid });
- });
-
- return Messages.findOneById(msg._id);
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/messages/index.js b/app/graphql/server/resolvers/messages/index.js
deleted file mode 100644
index 8a77927f9721..000000000000
--- a/app/graphql/server/resolvers/messages/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
-
-// queries
-import * as messages from './messages';
-// mutations
-import * as sendMessage from './sendMessage';
-import * as editMessage from './editMessage';
-import * as deleteMessage from './deleteMessage';
-import * as addReactionToMessage from './addReactionToMessage';
-// subscriptions
-import * as chatMessageAdded from './chatMessageAdded';
-// types
-import * as MessageType from './Message-type';
-import * as MessagesWithCursorType from './MessagesWithCursor-type';
-import * as MessageIdentifier from './MessageIdentifier-input';
-import * as ReactionType from './Reaction-type';
-
-export const schema = mergeTypes([
- // queries
- messages.schema,
- // mutations
- sendMessage.schema,
- editMessage.schema,
- deleteMessage.schema,
- addReactionToMessage.schema,
- // subscriptions
- chatMessageAdded.schema,
- // types
- MessageType.schema,
- MessagesWithCursorType.schema,
- MessageIdentifier.schema,
- ReactionType.schema,
-]);
-
-export const resolvers = mergeResolvers([
- // queries
- messages.resolver,
- // mutations
- sendMessage.resolver,
- editMessage.resolver,
- deleteMessage.resolver,
- addReactionToMessage.resolver,
- // subscriptions
- chatMessageAdded.resolver,
- // types
- MessageType.resolver,
-]);
diff --git a/app/graphql/server/resolvers/messages/messages.js b/app/graphql/server/resolvers/messages/messages.js
deleted file mode 100644
index e52e2d0ac783..000000000000
--- a/app/graphql/server/resolvers/messages/messages.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Rooms, Messages } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/messages/messages.graphqls';
-
-const resolver = {
- Query: {
- messages: authenticated((root, args, { user }) => {
- const messagesQuery = {};
- const messagesOptions = {
- sort: { ts: -1 },
- };
- const channelQuery = {};
- const isPagination = !!args.cursor || args.count > 0;
- let cursor;
-
- if (args.channelId) {
- // channelId
- channelQuery._id = args.channelId;
- } else if (args.directTo) {
- // direct message where directTo is a user id
- channelQuery.t = 'd';
- channelQuery.usernames = { $all: [args.directTo, user.username] };
- } else if (args.channelName) {
- // non-direct channel
- channelQuery.t = { $ne: 'd' };
- channelQuery.name = args.channelName;
- } else {
- console.error('messages query must be called with channelId or directTo');
- return null;
- }
-
- const channel = Rooms.findOne(channelQuery);
-
- let messagesArray = [];
-
- if (channel) {
- // cursor
- if (isPagination && args.cursor) {
- const cursorMsg = Messages.findOne(args.cursor, { fields: { ts: 1 } });
- messagesQuery.ts = { $lt: cursorMsg.ts };
- }
-
- // search
- if (typeof args.searchRegex === 'string') {
- messagesQuery.msg = {
- $regex: new RegExp(args.searchRegex, 'i'),
- };
- }
-
- // count
- if (isPagination && args.count) {
- messagesOptions.limit = args.count;
- }
-
- // exclude messages generated by server
- if (args.excludeServer === true) {
- messagesQuery.t = { $exists: false };
- }
-
- // look for messages that belongs to specific channel
- messagesQuery.rid = channel._id;
-
- const messages = Messages.find(messagesQuery, messagesOptions);
-
- messagesArray = messages.fetch();
-
- if (isPagination) {
- // oldest first (because of findOne)
- messagesOptions.sort.ts = 1;
-
- const firstMessage = Messages.findOne(messagesQuery, messagesOptions);
- const lastId = (messagesArray[messagesArray.length - 1] || {})._id;
-
- cursor = !lastId || lastId === firstMessage._id ? null : lastId;
- }
- }
-
- return {
- cursor,
- channel,
- messagesArray,
- };
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/messages/sendMessage.js b/app/graphql/server/resolvers/messages/sendMessage.js
deleted file mode 100644
index 2ed30c83c68e..000000000000
--- a/app/graphql/server/resolvers/messages/sendMessage.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { processWebhookMessage } from '../../../../lib';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/messages/sendMessage.graphqls';
-
-const resolver = {
- Mutation: {
- sendMessage: authenticated((root, { channelId, directTo, content }, { user }) => {
- const options = {
- text: content,
- channel: channelId || directTo,
- };
-
- const messageReturn = processWebhookMessage(options, user)[0];
-
- if (!messageReturn) {
- throw new Error('Unknown error');
- }
-
- return messageReturn.message;
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/users/User-type.js b/app/graphql/server/resolvers/users/User-type.js
deleted file mode 100644
index 0abf0c5751ee..000000000000
--- a/app/graphql/server/resolvers/users/User-type.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import property from 'lodash.property';
-
-import { Avatars, Rooms } from '../../../../models';
-import schema from '../../schemas/users/User-type.graphqls';
-
-const resolver = {
- User: {
- id: property('_id'),
- status: ({ status }) => status.toUpperCase(),
- avatar: async ({ _id }) => {
- // XXX js-accounts/graphql#16
- const avatar = await Avatars.model.rawCollection().findOne({
- userId: _id,
- }, { fields: { url: 1 } });
-
- if (avatar) {
- return avatar.url;
- }
- },
- channels: Meteor.bindEnvironment(({ _id }) => Rooms.findBySubscriptionUserId(_id).fetch()),
- directMessages: ({ username }) => Rooms.findDirectRoomContainingUsername(username).fetch(),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/resolvers/users/UserStatus-enum.js b/app/graphql/server/resolvers/users/UserStatus-enum.js
deleted file mode 100644
index c38f2765a17a..000000000000
--- a/app/graphql/server/resolvers/users/UserStatus-enum.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import schema from '../../schemas/users/UserStatus-enum.graphqls';
-
-export {
- schema,
-};
diff --git a/app/graphql/server/resolvers/users/index.js b/app/graphql/server/resolvers/users/index.js
deleted file mode 100644
index 7a6a31147b3e..000000000000
--- a/app/graphql/server/resolvers/users/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
-
-// mutations
-import * as setStatus from './setStatus';
-// types
-import * as UserType from './User-type';
-import * as UserStatus from './UserStatus-enum';
-
-export const schema = mergeTypes([
- // mutations
- setStatus.schema,
- // types
- UserType.schema,
- UserStatus.schema,
-]);
-
-export const resolvers = mergeResolvers([
- // mutations
- setStatus.resolver,
- // types
- UserType.resolver,
-]);
diff --git a/app/graphql/server/resolvers/users/setStatus.js b/app/graphql/server/resolvers/users/setStatus.js
deleted file mode 100644
index d4a68da651a6..000000000000
--- a/app/graphql/server/resolvers/users/setStatus.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Users } from '../../../../models';
-import { authenticated } from '../../helpers/authenticated';
-import schema from '../../schemas/users/setStatus.graphqls';
-
-const resolver = {
- Mutation: {
- setStatus: authenticated((root, { status }, { user }) => {
- Users.update(user._id, {
- $set: {
- status: status.toLowerCase(),
- },
- });
-
- return Users.findOne(user._id);
- }),
- },
-};
-
-export {
- schema,
- resolver,
-};
diff --git a/app/graphql/server/schema.js b/app/graphql/server/schema.js
deleted file mode 100644
index 5988bd1883b4..000000000000
--- a/app/graphql/server/schema.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { makeExecutableSchema } from 'graphql-tools';
-import { mergeTypes, mergeResolvers } from 'merge-graphql-schemas';
-
-import * as channels from './resolvers/channels';
-import * as messages from './resolvers/messages';
-import * as accounts from './resolvers/accounts';
-import * as users from './resolvers/users';
-
-const schema = mergeTypes([
- channels.schema,
- messages.schema,
- accounts.schema,
- users.schema,
-]);
-
-const resolvers = mergeResolvers([
- channels.resolvers,
- messages.resolvers,
- accounts.resolvers,
- users.resolvers,
-]);
-
-export const executableSchema = makeExecutableSchema({
- typeDefs: [schema],
- resolvers,
- logger: {
- log: (e) => console.log(e),
- },
-});
diff --git a/app/graphql/server/schemas/accounts/LoginResult-type.graphqls b/app/graphql/server/schemas/accounts/LoginResult-type.graphqls
deleted file mode 100644
index 0bdf1ed7e00b..000000000000
--- a/app/graphql/server/schemas/accounts/LoginResult-type.graphqls
+++ /dev/null
@@ -1,4 +0,0 @@
-type LoginResult {
- accessToken: String!
- refreshToken: String!
-}
diff --git a/app/graphql/server/schemas/accounts/OauthProvider-type.graphqls b/app/graphql/server/schemas/accounts/OauthProvider-type.graphqls
deleted file mode 100644
index c91fe5e6379a..000000000000
--- a/app/graphql/server/schemas/accounts/OauthProvider-type.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type OauthProvider {
- name: String!
-}
diff --git a/app/graphql/server/schemas/accounts/oauthProviders.graphqls b/app/graphql/server/schemas/accounts/oauthProviders.graphqls
deleted file mode 100644
index 9ba76de8adb8..000000000000
--- a/app/graphql/server/schemas/accounts/oauthProviders.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Query {
- oauthProviders: [OauthProvider]
-}
diff --git a/app/graphql/server/schemas/channels/Channel-type.graphqls b/app/graphql/server/schemas/channels/Channel-type.graphqls
deleted file mode 100644
index 03c4557918df..000000000000
--- a/app/graphql/server/schemas/channels/Channel-type.graphqls
+++ /dev/null
@@ -1,16 +0,0 @@
-type Channel {
- id: String!
- name: String
- description: String
- announcement: String
- topic: String
- members: [User]
- owners: [User]
- numberOfMembers: Int
- numberOfMessages: Int
- readOnly: Boolean
- direct: Boolean
- privateChannel: Boolean
- favourite: Boolean
- unseenMessages: Int
-}
diff --git a/app/graphql/server/schemas/channels/ChannelFilter-input.graphqls b/app/graphql/server/schemas/channels/ChannelFilter-input.graphqls
deleted file mode 100644
index a00850e371cc..000000000000
--- a/app/graphql/server/schemas/channels/ChannelFilter-input.graphqls
+++ /dev/null
@@ -1,6 +0,0 @@
-input ChannelFilter {
- nameFilter: String
- privacy: Privacy
- joinedChannels: Boolean
- sortBy: ChannelSort
-}
diff --git a/app/graphql/server/schemas/channels/ChannelNameAndDirect-input.graphqls b/app/graphql/server/schemas/channels/ChannelNameAndDirect-input.graphqls
deleted file mode 100644
index 139567e862e9..000000000000
--- a/app/graphql/server/schemas/channels/ChannelNameAndDirect-input.graphqls
+++ /dev/null
@@ -1,4 +0,0 @@
-input ChannelNameAndDirect {
- name: String!
- direct: Boolean!
-}
diff --git a/app/graphql/server/schemas/channels/ChannelSort-enum.graphqls b/app/graphql/server/schemas/channels/ChannelSort-enum.graphqls
deleted file mode 100644
index 8a2c2902e6db..000000000000
--- a/app/graphql/server/schemas/channels/ChannelSort-enum.graphqls
+++ /dev/null
@@ -1,4 +0,0 @@
-enum ChannelSort {
- NAME
- NUMBER_OF_MESSAGES
-}
diff --git a/app/graphql/server/schemas/channels/Privacy-enum.graphqls b/app/graphql/server/schemas/channels/Privacy-enum.graphqls
deleted file mode 100644
index f28a57d51c28..000000000000
--- a/app/graphql/server/schemas/channels/Privacy-enum.graphqls
+++ /dev/null
@@ -1,5 +0,0 @@
-enum Privacy {
- PRIVATE
- PUBLIC
- ALL
-}
diff --git a/app/graphql/server/schemas/channels/channelByName.graphqls b/app/graphql/server/schemas/channels/channelByName.graphqls
deleted file mode 100644
index e301ce38f7f6..000000000000
--- a/app/graphql/server/schemas/channels/channelByName.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Query {
- channelByName(name: String!): Channel
-}
diff --git a/app/graphql/server/schemas/channels/channels.graphqls b/app/graphql/server/schemas/channels/channels.graphqls
deleted file mode 100644
index 6e4ac5608249..000000000000
--- a/app/graphql/server/schemas/channels/channels.graphqls
+++ /dev/null
@@ -1,7 +0,0 @@
-type Query {
- channels(filter: ChannelFilter = {
- privacy: ALL,
- joinedChannels: false,
- sortBy: NAME
- }): [Channel]
-}
diff --git a/app/graphql/server/schemas/channels/channelsByUser.graphqls b/app/graphql/server/schemas/channels/channelsByUser.graphqls
deleted file mode 100644
index 8dfe20a071c5..000000000000
--- a/app/graphql/server/schemas/channels/channelsByUser.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Query {
- channelsByUser(userId: String!): [Channel]
-}
diff --git a/app/graphql/server/schemas/channels/createChannel.graphqls b/app/graphql/server/schemas/channels/createChannel.graphqls
deleted file mode 100644
index 85317c86603a..000000000000
--- a/app/graphql/server/schemas/channels/createChannel.graphqls
+++ /dev/null
@@ -1,8 +0,0 @@
-type Mutation {
- createChannel(
- name: String!,
- private: Boolean = false,
- readOnly: Boolean = false,
- membersId: [String!]
- ): Channel
-}
diff --git a/app/graphql/server/schemas/channels/deleteChannel.graphqls b/app/graphql/server/schemas/channels/deleteChannel.graphqls
deleted file mode 100644
index bf11cc4fbe47..000000000000
--- a/app/graphql/server/schemas/channels/deleteChannel.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- deleteChannel(channelId: String!): Boolean
-}
diff --git a/app/graphql/server/schemas/channels/directChannel.graphqls b/app/graphql/server/schemas/channels/directChannel.graphqls
deleted file mode 100644
index 4e41994bce80..000000000000
--- a/app/graphql/server/schemas/channels/directChannel.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Query {
- directChannel(username: String, channelId: String): Channel
-}
diff --git a/app/graphql/server/schemas/channels/hideChannel.graphqls b/app/graphql/server/schemas/channels/hideChannel.graphqls
deleted file mode 100644
index 5ea9517f5741..000000000000
--- a/app/graphql/server/schemas/channels/hideChannel.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- hideChannel(channelId: String!): Boolean
-}
diff --git a/app/graphql/server/schemas/channels/leaveChannel.graphqls b/app/graphql/server/schemas/channels/leaveChannel.graphqls
deleted file mode 100644
index e6ceb4075c4e..000000000000
--- a/app/graphql/server/schemas/channels/leaveChannel.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- leaveChannel(channelId: String!): Boolean
-}
diff --git a/app/graphql/server/schemas/messages/Message-type.graphqls b/app/graphql/server/schemas/messages/Message-type.graphqls
deleted file mode 100644
index 8ccfdebfbf1d..000000000000
--- a/app/graphql/server/schemas/messages/Message-type.graphqls
+++ /dev/null
@@ -1,15 +0,0 @@
-type Message {
- id: String
- author: User
- content: String
- channel: Channel
- creationTime: Float
- # Message sent by server e.g. User joined channel
- fromServer: Boolean
- type: String
- # List of mentioned users
- userRef: [User]
- # list of mentioned channels
- channelRef: [Channel]
- reactions: [Reaction]
-}
diff --git a/app/graphql/server/schemas/messages/MessageIdentifier-input.graphqls b/app/graphql/server/schemas/messages/MessageIdentifier-input.graphqls
deleted file mode 100644
index 88fbe90711cf..000000000000
--- a/app/graphql/server/schemas/messages/MessageIdentifier-input.graphqls
+++ /dev/null
@@ -1,4 +0,0 @@
-input MessageIdentifier {
- channelId: String!
- messageId: String!
-}
diff --git a/app/graphql/server/schemas/messages/MessagesWithCursor-type.graphqls b/app/graphql/server/schemas/messages/MessagesWithCursor-type.graphqls
deleted file mode 100644
index e890725f6efb..000000000000
--- a/app/graphql/server/schemas/messages/MessagesWithCursor-type.graphqls
+++ /dev/null
@@ -1,5 +0,0 @@
-type MessagesWithCursor {
- cursor: String
- channel: Channel
- messagesArray: [Message]
-}
diff --git a/app/graphql/server/schemas/messages/Reaction-type.graphqls b/app/graphql/server/schemas/messages/Reaction-type.graphqls
deleted file mode 100644
index e6eedf75d1e1..000000000000
--- a/app/graphql/server/schemas/messages/Reaction-type.graphqls
+++ /dev/null
@@ -1,4 +0,0 @@
-type Reaction {
- username: String
- icon: String
-}
diff --git a/app/graphql/server/schemas/messages/addReactionToMessage.graphqls b/app/graphql/server/schemas/messages/addReactionToMessage.graphqls
deleted file mode 100644
index af9f913a1644..000000000000
--- a/app/graphql/server/schemas/messages/addReactionToMessage.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- addReactionToMessage(id: MessageIdentifier!, icon: String!, shouldReact: Boolean): Message
-}
diff --git a/app/graphql/server/schemas/messages/chatMessageAdded.graphqls b/app/graphql/server/schemas/messages/chatMessageAdded.graphqls
deleted file mode 100644
index d05c00afc3bc..000000000000
--- a/app/graphql/server/schemas/messages/chatMessageAdded.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Subscription {
- chatMessageAdded(channelId: String, directTo: String): Message
-}
diff --git a/app/graphql/server/schemas/messages/deleteMessage.graphqls b/app/graphql/server/schemas/messages/deleteMessage.graphqls
deleted file mode 100644
index f298a14ebf98..000000000000
--- a/app/graphql/server/schemas/messages/deleteMessage.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- deleteMessage(id: MessageIdentifier!): Message
-}
diff --git a/app/graphql/server/schemas/messages/editMessage.graphqls b/app/graphql/server/schemas/messages/editMessage.graphqls
deleted file mode 100644
index 19900c58b272..000000000000
--- a/app/graphql/server/schemas/messages/editMessage.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- editMessage(id: MessageIdentifier!, content: String!): Message
-}
diff --git a/app/graphql/server/schemas/messages/messages.graphqls b/app/graphql/server/schemas/messages/messages.graphqls
deleted file mode 100644
index a81fe2186ca7..000000000000
--- a/app/graphql/server/schemas/messages/messages.graphqls
+++ /dev/null
@@ -1,11 +0,0 @@
-type Query {
- messages(
- channelId: String,
- channelName: String,
- directTo: String,
- cursor: String,
- count: Int,
- searchRegex: String,
- excludeServer: Boolean
- ): MessagesWithCursor
-}
diff --git a/app/graphql/server/schemas/messages/sendMessage.graphqls b/app/graphql/server/schemas/messages/sendMessage.graphqls
deleted file mode 100644
index 78933ec6a763..000000000000
--- a/app/graphql/server/schemas/messages/sendMessage.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- sendMessage(channelId: String, directTo: String, content: String!): Message
-}
diff --git a/app/graphql/server/schemas/users/User-type.graphqls b/app/graphql/server/schemas/users/User-type.graphqls
deleted file mode 100644
index 2a345415227e..000000000000
--- a/app/graphql/server/schemas/users/User-type.graphqls
+++ /dev/null
@@ -1,8 +0,0 @@
-extend type User {
- status: UserStatus
- avatar: String
- name: String
- lastLogin: String
- channels: [Channel]
- directMessages: [Channel]
-}
diff --git a/app/graphql/server/schemas/users/UserStatus-enum.graphqls b/app/graphql/server/schemas/users/UserStatus-enum.graphqls
deleted file mode 100644
index a360cc2a72b9..000000000000
--- a/app/graphql/server/schemas/users/UserStatus-enum.graphqls
+++ /dev/null
@@ -1,7 +0,0 @@
-enum UserStatus {
- ONLINE
- AWAY
- BUSY
- INVISIBLE
- OFFLINE
-}
diff --git a/app/graphql/server/schemas/users/setStatus.graphqls b/app/graphql/server/schemas/users/setStatus.graphqls
deleted file mode 100644
index 7beb3512a91b..000000000000
--- a/app/graphql/server/schemas/users/setStatus.graphqls
+++ /dev/null
@@ -1,3 +0,0 @@
-type Mutation {
- setStatus(status: UserStatus!): User
-}
diff --git a/app/graphql/server/settings.js b/app/graphql/server/settings.js
deleted file mode 100644
index b1d0b7021b08..000000000000
--- a/app/graphql/server/settings.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { settings } from '../../settings';
-
-settings.addGroup('General', function() {
- this.section('GraphQL API', function() {
- this.add('Graphql_Enabled', false, { type: 'boolean', public: false });
- this.add('Graphql_CORS', true, { type: 'boolean', public: false, enableQuery: { _id: 'Graphql_Enabled', value: true } });
- this.add('Graphql_Subscription_Port', 3100, { type: 'int', public: false, enableQuery: { _id: 'Graphql_Enabled', value: true } });
- });
-});
diff --git a/app/graphql/server/subscriptions.js b/app/graphql/server/subscriptions.js
deleted file mode 100644
index d86d23f85d05..000000000000
--- a/app/graphql/server/subscriptions.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { PubSub } from 'graphql-subscriptions';
-
-export const pubsub = new PubSub();
diff --git a/app/importer/client/admin/adminImport.js b/app/importer/client/admin/adminImport.js
index 834a3ecf66da..f04242721898 100644
--- a/app/importer/client/admin/adminImport.js
+++ b/app/importer/client/admin/adminImport.js
@@ -5,7 +5,7 @@ import { Importers } from '..';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { hasRole } from '../../../authorization';
import { t, handleError } from '../../../utils';
diff --git a/app/importer/client/admin/adminImportPrepare.js b/app/importer/client/admin/adminImportPrepare.js
index 82cf2be02548..269057c3820b 100644
--- a/app/importer/client/admin/adminImportPrepare.js
+++ b/app/importer/client/admin/adminImportPrepare.js
@@ -6,7 +6,7 @@ import { Importers } from '..';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import toastr from 'toastr';
import { hasRole } from '../../../authorization';
diff --git a/app/integrations/client/views/integrations.js b/app/integrations/client/views/integrations.js
index aaf73c2b183e..a1432daaad01 100644
--- a/app/integrations/client/views/integrations.js
+++ b/app/integrations/client/views/integrations.js
@@ -1,5 +1,5 @@
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import moment from 'moment';
diff --git a/app/integrations/client/views/integrationsIncoming.js b/app/integrations/client/views/integrationsIncoming.js
index e419033ff408..f1d9a48842c0 100644
--- a/app/integrations/client/views/integrationsIncoming.js
+++ b/app/integrations/client/views/integrationsIncoming.js
@@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import hljs from 'highlight.js';
import toastr from 'toastr';
diff --git a/app/integrations/client/views/integrationsOutgoing.js b/app/integrations/client/views/integrationsOutgoing.js
index ba136d3fbc3f..b41788117c3c 100644
--- a/app/integrations/client/views/integrationsOutgoing.js
+++ b/app/integrations/client/views/integrationsOutgoing.js
@@ -3,7 +3,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
import { Random } from 'meteor/random';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import hljs from 'highlight.js';
import toastr from 'toastr';
diff --git a/app/integrations/client/views/integrationsOutgoingHistory.js b/app/integrations/client/views/integrationsOutgoingHistory.js
index 02252347c462..a6b37dcb68f0 100644
--- a/app/integrations/client/views/integrationsOutgoingHistory.js
+++ b/app/integrations/client/views/integrationsOutgoingHistory.js
@@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import _ from 'underscore';
import hljs from 'highlight.js';
diff --git a/app/lib/client/CustomTranslations.js b/app/lib/client/CustomTranslations.js
index 2530da32269e..6c128512c4dd 100644
--- a/app/lib/client/CustomTranslations.js
+++ b/app/lib/client/CustomTranslations.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import { applyCustomTranslations } from '../../utils';
diff --git a/app/lib/lib/roomTypes/direct.js b/app/lib/lib/roomTypes/direct.js
index 8499332c8918..a0226c51f943 100644
--- a/app/lib/lib/roomTypes/direct.js
+++ b/app/lib/lib/roomTypes/direct.js
@@ -101,10 +101,6 @@ export class DirectMessageRoomType extends RoomTypeConfig {
return Session.get(`user_${ subscription.name }_status_text`);
}
- getDisplayName(room) {
- return room.usernames.join(' x ');
- }
-
allowRoomSettingChange(room, setting) {
switch (setting) {
case RoomSettingsEnum.NAME:
diff --git a/app/lib/server/functions/cleanRoomHistory.js b/app/lib/server/functions/cleanRoomHistory.js
index 6aacf66444eb..e1aed74cbfa2 100644
--- a/app/lib/server/functions/cleanRoomHistory.js
+++ b/app/lib/server/functions/cleanRoomHistory.js
@@ -1,4 +1,4 @@
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { deleteRoom } from './deleteRoom';
import { FileUpload } from '../../../file-upload';
diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js
index fe54a6f57c55..c463f94b7a88 100644
--- a/app/lib/server/functions/deleteUser.js
+++ b/app/lib/server/functions/deleteUser.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { FileUpload } from '../../../file-upload';
import { Users, Subscriptions, Messages, Rooms, Integrations, FederationPeers } from '../../../models';
diff --git a/app/lib/server/functions/loadNewsfeedHistory.js b/app/lib/server/functions/loadNewsfeedHistory.js
index 49d287a8b312..84bc876b3cbb 100644
--- a/app/lib/server/functions/loadNewsfeedHistory.js
+++ b/app/lib/server/functions/loadNewsfeedHistory.js
@@ -79,7 +79,6 @@ export const loadNewsfeedHistory = function loadNewsfeedHistory({ userId, end, l
firstUnread = unreadMessages.fetch()[0];
unreadNotLoaded = unreadMessages.count();
}
-
}
}
diff --git a/app/lib/server/functions/notifications/email.js b/app/lib/server/functions/notifications/email.js
index a0ce6309ef25..80584abc4a61 100644
--- a/app/lib/server/functions/notifications/email.js
+++ b/app/lib/server/functions/notifications/email.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import s from 'underscore.string';
import * as Mailer from '../../../../mailer';
diff --git a/app/lib/server/functions/notifications/index.js b/app/lib/server/functions/notifications/index.js
index 92a3733961f9..6dbe07fb0402 100644
--- a/app/lib/server/functions/notifications/index.js
+++ b/app/lib/server/functions/notifications/index.js
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import s from 'underscore.string';
import { settings } from '../../../../settings';
diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js
index a1882ebae2ee..3b2ad47cc86f 100644
--- a/app/lib/server/functions/sendMessage.js
+++ b/app/lib/server/functions/sendMessage.js
@@ -6,6 +6,8 @@ import { callbacks } from '../../../callbacks';
import { Messages } from '../../../models';
import { Apps } from '../../../apps/server';
import { Markdown } from '../../../markdown/server';
+import { isURL } from '../../../utils/lib/isURL';
+import { FileUpload } from '../../../file-upload/server';
/**
* IMPORTANT
@@ -16,9 +18,13 @@ import { Markdown } from '../../../markdown/server';
* is going to be rendered in the href attribute of a
* link.
*/
-const ValidHref = Match.Where((value) => {
+const ValidLinkParam = Match.Where((value) => {
check(value, String);
+ if (!isURL(value) && !value.startsWith(FileUpload.getPath())) {
+ throw new Error('Invalid href value provided');
+ }
+
if (/^javascript:/i.test(value)) {
throw new Error('Invalid href value provided');
}
@@ -57,8 +63,8 @@ const validateAttachmentsActions = (attachmentActions) => {
check(attachmentActions, objectMaybeIncluding({
type: String,
text: String,
- url: ValidHref,
- image_url: String,
+ url: ValidLinkParam,
+ image_url: ValidLinkParam,
is_webview: Boolean,
webview_height_ratio: String,
msg: String,
@@ -73,26 +79,26 @@ const validateAttachment = (attachment) => {
color: String,
text: String,
ts: Match.OneOf(String, Match.Integer),
- thumb_url: String,
+ thumb_url: ValidLinkParam,
button_alignment: String,
actions: [Match.Any],
- message_link: ValidHref,
+ message_link: ValidLinkParam,
collapsed: Boolean,
author_name: String,
- author_link: ValidHref,
- author_icon: String,
+ author_link: ValidLinkParam,
+ author_icon: ValidLinkParam,
title: String,
- title_link: ValidHref,
+ title_link: ValidLinkParam,
title_link_download: Boolean,
image_dimensions: Object,
- image_url: String,
+ image_url: ValidLinkParam,
image_preview: String,
image_type: String,
image_size: Number,
- audio_url: String,
+ audio_url: ValidLinkParam,
audio_type: String,
audio_size: Number,
- video_url: String,
+ video_url: ValidLinkParam,
video_type: String,
video_size: Number,
fields: [Match.Any],
@@ -116,7 +122,7 @@ const validateMessage = (message) => {
text: String,
alias: String,
emoji: String,
- avatar: String,
+ avatar: ValidLinkParam,
attachments: [Match.Any],
}));
diff --git a/app/lib/server/methods/addUsersToRoom.js b/app/lib/server/methods/addUsersToRoom.js
index d414ff4eca44..63eaa7594a83 100644
--- a/app/lib/server/methods/addUsersToRoom.js
+++ b/app/lib/server/methods/addUsersToRoom.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Random } from 'meteor/random';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Rooms, Subscriptions, Users } from '../../../models';
import { hasPermission } from '../../../authorization';
diff --git a/app/lib/server/methods/filterATAllTag.js b/app/lib/server/methods/filterATAllTag.js
index 3966a5c90bd7..9ada82426f3b 100644
--- a/app/lib/server/methods/filterATAllTag.js
+++ b/app/lib/server/methods/filterATAllTag.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import moment from 'moment';
diff --git a/app/lib/server/methods/filterATHereTag.js b/app/lib/server/methods/filterATHereTag.js
index e105a8d76aad..79198e1aaf5d 100644
--- a/app/lib/server/methods/filterATHereTag.js
+++ b/app/lib/server/methods/filterATHereTag.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import moment from 'moment';
diff --git a/app/lib/server/methods/sendMessage.js b/app/lib/server/methods/sendMessage.js
index 67b20c05327c..4cb83fc5c710 100644
--- a/app/lib/server/methods/sendMessage.js
+++ b/app/lib/server/methods/sendMessage.js
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { Random } from 'meteor/random';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import moment from 'moment';
import { hasPermission } from '../../../authorization';
@@ -89,6 +89,10 @@ Meteor.methods({
throw new Meteor.Error('error-not-allowed');
}
+ if (error.error === 'error-max-broadcast-limit') {
+ throw new Meteor.Error('error-max-broadcast-limit');
+ }
+
SystemLogger.error('Error sending message:', error);
Notifications.notifyUser(uid, 'message', {
diff --git a/app/lib/server/oauth/facebook.js b/app/lib/server/oauth/facebook.js
index f19aa01c622b..547eee346eb6 100644
--- a/app/lib/server/oauth/facebook.js
+++ b/app/lib/server/oauth/facebook.js
@@ -45,10 +45,9 @@ registerAccessTokenService('facebook', function(options) {
accessToken: String,
secret: String,
expiresIn: Match.Integer,
- identity: Match.Maybe(Object),
}));
- const identity = options.identity || getIdentity(options.accessToken, whitelisted, options.secret);
+ const identity = getIdentity(options.accessToken, whitelisted, options.secret);
const serviceData = {
accessToken: options.accessToken,
diff --git a/app/lib/server/oauth/google.js b/app/lib/server/oauth/google.js
index 848aa1c1c1b0..98b920a23550 100644
--- a/app/lib/server/oauth/google.js
+++ b/app/lib/server/oauth/google.js
@@ -35,7 +35,7 @@ registerAccessTokenService('google', function(options) {
identity: Match.Maybe(Object),
}));
- const identity = options.identity || getIdentity(options.accessToken);
+ const identity = getIdentity(options.accessToken);
const serviceData = {
accessToken: options.accessToken,
diff --git a/app/lib/server/oauth/twitter.js b/app/lib/server/oauth/twitter.js
index d93499fb57f8..132af472a03d 100644
--- a/app/lib/server/oauth/twitter.js
+++ b/app/lib/server/oauth/twitter.js
@@ -38,10 +38,9 @@ registerAccessTokenService('twitter', function(options) {
appId: String,
accessTokenSecret: String,
expiresIn: Match.Integer,
- identity: Match.Maybe(Object),
}));
- const identity = options.identity || getIdentity(options.accessToken, options.appId, options.appSecret, options.accessTokenSecret);
+ const identity = getIdentity(options.accessToken, options.appId, options.appSecret, options.accessTokenSecret);
const serviceData = {
accessToken: options.accessToken,
diff --git a/app/livechat/client/lib/chartHandler.js b/app/livechat/client/lib/chartHandler.js
index 4b65d57737a8..a58592df5391 100644
--- a/app/livechat/client/lib/chartHandler.js
+++ b/app/livechat/client/lib/chartHandler.js
@@ -1,5 +1,5 @@
import Chart from 'chart.js';
-import { TAPi18n } from 'meteor/tap:i18n';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
const lineChartConfiguration = ({ legends = false, anim = false, smallTicks = false }) => {
const config = {
diff --git a/app/livechat/client/stylesheets/livechat.less b/app/livechat/client/stylesheets/livechat.less
index daa102095745..af57cff17575 100644
--- a/app/livechat/client/stylesheets/livechat.less
+++ b/app/livechat/client/stylesheets/livechat.less
@@ -60,510 +60,8 @@
.livechat-content {
display: flex;
flex-direction: row;
-}
-
-.livechat-settings-div,
-.livechat-preview-div {
- flex: 1 1;
- margin-bottom: 0 !important;
- padding: 1em;
-}
-
-.livechat-settings-div {
- border-right: 1px solid #cccccc;
-}
-
-.livechat-preview {
- width: 340px;
- height: 350px;
- margin: 0 auto;
- border-bottom: 1px solid #cccccc;
- position: relative;
-
- .preview-wrapper {
- position: absolute;
- width: 100%;
- padding: 0 20px;
- height: 300px;
- bottom: 0;
- font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif, "Meiryo UI";
- font-size: 0.8rem;
- color: @primary-font-color;
- -webkit-font-smoothing: antialiased;
- line-height: 1rem;
-
- input,
- button,
- select,
- textarea {
- font-family: inherit;
- font-size: inherit;
- line-height: inherit;
- padding: 5px;
- margin: 5px 0;
- border: 1px solid #e7e7e7;
- border-radius: 5px;
- outline: none;
- }
-
- input {
- height: 28px;
- }
-
- input:focus {
- outline: none;
- box-shadow: 0 0 0;
- }
-
- .button {
- &:extend(.unselectable);
- display: inline-block;
- padding: 9px 12px;
- font-weight: 500;
- font-size: 13px;
- margin: 4px;
- text-transform: uppercase;
- word-spacing: 0;
- box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.125);
- border: none;
- border-radius: 0;
- line-height: 16px;
- position: relative;
- cursor: pointer;
- color: rgba(255, 255, 255, 0.85);
-
- span {
- position: relative;
- z-index: 2;
- }
-
- &::before {
- background-color: rgba(0, 0, 0, 0.1);
- content: " ";
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- z-index: 1;
- transition: opacity 0.1s ease-out;
- }
-
- &:hover {
- text-decoration: none;
- color: #ffffff;
-
- &::before {
- opacity: 1;
- }
- }
-
- &.clean {
- font-size: 14px;
- box-shadow: 0 0 3px rgba(0, 0, 0, 0.08);
-
- &.primary {
- font-weight: 600;
- }
- }
-
- &.button-block {
- display: block;
- width: 100%;
- }
- }
-
- .livechat-room {
- display: flex;
- flex-direction: column;
- height: 100%;
-
- .title {
- flex: 1 0 @header-min-height;
- line-height: @header-min-height;
- border-top-right-radius: 5px;
- border-top-left-radius: 5px;
- color: #ffffff;
- z-index: 10;
- cursor: pointer;
-
- h1 {
- margin: 0;
- padding: 0 5px;
- font-size: 9pt;
- display: inline-block;
- text-transform: none;
- }
-
- .toolbar {
- display: inline-block;
- float: right;
- padding-right: 5px;
- }
- }
-
- .messages {
- flex: 1 1 100%;
- background-color: #ffffff;
- border-left: 1px solid #e7e7e7;
- border-right: 1px solid #e7e7e7;
- overflow-y: auto;
-
- .wrapper {
- padding-bottom: 6px;
-
- ul {
- list-style-type: none;
- padding: 0;
-
- li {
- padding: 0;
- }
- }
-
- .message {
- font-size: 12px;
- padding-left: 40px;
- position: relative;
- line-height: 18px;
- margin: 12px 10px 0;
- min-height: 36px;
-
- &:nth-child(1) {
- margin-top: 0;
- }
-
- &.new-day {
- margin-top: 60px;
-
- &::before {
- content: attr(data-date);
- display: block;
- position: absolute;
- top: -30px;
- left: calc(~'50% - 70px');
- font-size: 10px;
- font-weight: 600;
- text-align: center;
- color: @secondary-font-color;
- z-index: 10;
- padding: 0 10px;
- background-color: #ffffff;
- min-width: 120px;
- }
-
- &::after {
- content: " ";
- display: block;
- position: absolute;
- top: -20px;
- left: 0;
- width: 100%;
- border-top: 1px solid #dddddd;
- }
- }
-
- .edit-message {
- display: none;
- cursor: pointer;
- }
-
- &.own:hover:not(.system) .edit-message {
- display: inline-block;
- }
-
- .delete-message {
- display: none;
- cursor: pointer;
- }
-
- &.own:hover:not(.system) .delete-message {
- display: inline-block;
- }
-
- .user {
- display: inline-block;
- font-weight: 600;
- color: #444444;
- margin-right: 5px;
- outline: none;
-
- &:hover {
- color: #333333;
- }
- }
-
- .thumb {
- position: absolute;
- left: 0;
- top: 0;
- display: block;
- width: 30px;
- height: 30px;
- }
-
- .info {
- font-size: 10px;
- color: @info-font-color;
- }
-
- &.sequential {
- padding-top: 5px;
- margin-top: 0;
- margin-bottom: 0;
- min-height: 20px;
-
- .user {
- display: none;
- }
-
- .thumb {
- display: none;
- }
-
- .info {
- position: absolute;
- text-align: right;
- left: -20px;
- width: 55px;
-
- .time {
- display: none;
- }
-
- .edited {
- display: inline-block;
- }
-
- .edit-message {
- float: left;
- margin-left: 1px;
- }
-
- .delete-message {
- float: left;
- }
- }
-
- &:hover {
- .time {
- display: inline-block;
- }
-
- .edited {
- display: none;
- }
- }
- }
-
- &.system {
- .body {
- color: @info-font-color;
- font-style: italic;
- text-transform: lowercase;
-
- em {
- font-weight: 600;
- }
- }
- }
-
- .avatar-initials {
- line-height: 40px;
- }
-
- a {
- color: @link-font-color;
- font-weight: 400;
-
- &:hover {
- color: darken(@link-font-color, 10%);
- text-decoration: underline;
- }
- }
-
- .body {
- opacity: 1;
- transition: opacity 1s linear;
- }
-
- &.temp .body {
- opacity: 0.5;
- }
-
- &.msg-error .body {
- text-decoration: line-through;
- }
-
- .avatar .avatar-image {
- height: 100%;
- width: 100%;
- min-height: 20px;
- min-width: 20px;
- display: block;
- position: relative;
- background-color: transparent;
- background-size: cover;
- background-repeat: no-repeat;
- background-position: center;
- border-radius: 4px;
- }
- }
- }
- .new-message {
- margin: 0 -65px;
- position: absolute;
- background: #428bca;
- border-radius: 20px;
- width: 130px;
- height: 30px;
- text-align: center;
- color: #ffffff;
- line-height: 30px;
- font-size: 0.8em;
- cursor: pointer;
- bottom: 8px;
- left: 50%;
- z-index: 5;
- transition: transform 0.3s ease-out;
- transform: translateY(-40px);
-
- &.not {
- transform: translateY(100%);
- }
- }
-
- .error {
- bottom: 40px;
- position: fixed;
- width: 100%;
- background-color: #f7d799;
- padding: 5px;
- z-index: 8;
- transition: transform 0.2s ease-out;
- transform: translateY(100%);
-
- &.show {
- transform: translateY(0);
- }
- }
- }
-
- .footer {
- flex: 1 0 @footer-min-height;
- z-index: 10;
- background-color: #fcfcfc;
- border-top: 1px solid #e7e7e7;
- border-left: 1px solid #e7e7e7;
- border-right: 1px solid #e7e7e7;
-
- .input-wrapper {
- padding: 6px 6px 0;
- padding-right: 30px;
-
- textarea {
- display: block;
- padding: 6px 8px;
- padding-right: 38px;
- overflow-y: auto;
- resize: none;
- border: 1px solid #e7e7e7;
- border-radius: 5px;
- max-height: 200px;
- width: 100%;
- font-size: 12px;
- -webkit-appearance: none;
- height: 28px;
- line-height: normal;
- background-color: #ffffff;
- position: relative;
- outline: none;
- }
- }
-
- .send-button {
- float: right;
- position: relative;
- right: 7px;
- top: -28px;
- color: @secondary-font-color;
- cursor: pointer;
- transition: color 0.15s ease-out;
-
- &:hover {
- color: @primary-font-color;
- }
- }
- }
-
- .offline {
- flex: 1 1 100%;
- background-color: white;
- padding: 1em 10px;
- border-left: 1px solid #e7e7e7;
- border-right: 1px solid #e7e7e7;
-
- .offline-message {
- padding: 1em 0;
- }
-
- .message-sent {
- text-align: center;
- }
-
- form {
- input,
- textarea {
- display: block;
- width: 100%;
- }
-
- textarea {
- height: 3.5em;
- }
-
- .buttons {
- text-align: center;
- }
-
- .error {
- display: none;
- background-color: #f7d799;
- padding: 5px;
-
- &.show {
- display: block;
- }
- }
- }
- }
- }
- }
-
- &.closed,
- &.closed-offline {
- .preview-wrapper {
- height: 32px;
-
- .livechat-room .title .toolbar {
- display: none;
- }
-
- .messages {
- display: none;
- }
-
- .footer {
- display: none;
- }
-
- .offline {
- display: none;
- }
- }
- }
+ max-width: 960px;
}
.department-agents {
diff --git a/app/livechat/client/views/app/livechatAppearance.html b/app/livechat/client/views/app/livechatAppearance.html
index a6885d18a392..08353afc511d 100644
--- a/app/livechat/client/views/app/livechatAppearance.html
+++ b/app/livechat/client/views/app/livechatAppearance.html
@@ -2,213 +2,124 @@
{{#requiresPermission 'view-livechat-manager'}}