diff --git a/apps/meteor/app/api/server/lib/integrations.ts b/apps/meteor/app/api/server/lib/integrations.ts index 5ac478353807..c785984380e5 100644 --- a/apps/meteor/app/api/server/lib/integrations.ts +++ b/apps/meteor/app/api/server/lib/integrations.ts @@ -24,7 +24,7 @@ export const findOneIntegration = async ({ }: { userId: string; integrationId: string; - createdBy: IUser; + createdBy?: IUser['_id']; }): Promise => { const integration = await Integrations.findOneByIdAndCreatedByIfExists({ _id: integrationId, diff --git a/apps/meteor/app/api/server/v1/integrations.js b/apps/meteor/app/api/server/v1/integrations.ts similarity index 55% rename from apps/meteor/app/api/server/v1/integrations.js rename to apps/meteor/app/api/server/v1/integrations.ts index 90b5f8d09b02..459f3e40a836 100644 --- a/apps/meteor/app/api/server/v1/integrations.js +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -1,5 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { IIntegration } from '@rocket.chat/core-typings'; +import { + isIntegrationsCreateProps, + isIntegrationsHistoryProps, + isIntegrationsRemoveProps, + isIntegrationsGetProps, + isIntegrationsUpdateProps, +} from '@rocket.chat/rest-typings'; import { hasAtLeastOnePermission } from '../../../authorization/server'; import { Integrations, IntegrationHistory } from '../../../models/server/raw'; @@ -12,45 +20,32 @@ import { findOneIntegration } from '../lib/integrations'; API.v1.addRoute( 'integrations.create', - { authRequired: true }, + { authRequired: true, validateParams: isIntegrationsCreateProps }, { post() { - check( - this.bodyParams, - Match.ObjectIncluding({ - type: String, - name: String, - enabled: Boolean, - username: String, - urls: Match.Maybe([String]), - channel: String, - event: Match.Maybe(String), - triggerWords: Match.Maybe([String]), - alias: Match.Maybe(String), - avatar: Match.Maybe(String), - emoji: Match.Maybe(String), - token: Match.Maybe(String), - scriptEnabled: Boolean, - script: Match.Maybe(String), - targetChannel: Match.Maybe(String), - }), - ); - - let integration; - - switch (this.bodyParams.type) { - case 'webhook-outgoing': - Meteor.runAsUser(this.userId, () => { - integration = Meteor.call('addOutgoingIntegration', this.bodyParams); - }); - break; - case 'webhook-incoming': - Meteor.runAsUser(this.userId, () => { - integration = Meteor.call('addIncomingIntegration', this.bodyParams); - }); - break; - default: - return API.v1.failure('Invalid integration type.'); + const { userId, bodyParams } = this; + + const integration = ((): IIntegration | undefined => { + let integration: IIntegration | undefined; + + switch (bodyParams.type) { + case 'webhook-outgoing': + Meteor.runAsUser(userId, () => { + integration = Meteor.call('addOutgoingIntegration', bodyParams); + }); + break; + case 'webhook-incoming': + Meteor.runAsUser(userId, () => { + integration = Meteor.call('addIncomingIntegration', bodyParams); + }); + break; + } + + return integration; + })(); + + if (!integration) { + return API.v1.failure('Invalid integration type.'); } return API.v1.success({ integration }); @@ -60,21 +55,23 @@ API.v1.addRoute( API.v1.addRoute( 'integrations.history', - { authRequired: true }, + { authRequired: true, validateParams: isIntegrationsHistoryProps }, { get() { - if (!hasAtLeastOnePermission(this.userId, ['manage-outgoing-integrations', 'manage-own-outgoing-integrations'])) { + const { userId, queryParams } = this; + + if (!hasAtLeastOnePermission(userId, ['manage-outgoing-integrations', 'manage-own-outgoing-integrations'])) { return API.v1.unauthorized(); } - if (!this.queryParams.id || this.queryParams.id.trim() === '') { + if (!queryParams.id || queryParams.id.trim() === '') { return API.v1.failure('Invalid integration id.'); } - const { id } = this.queryParams; + const { id } = queryParams; const { offset, count } = this.getPaginationItems(); const { sort, fields: projection, query } = this.parseJsonQuery(); - const ourQuery = Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(this.userId, id), query); + const ourQuery = Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(userId, id), query); const cursor = IntegrationHistory.find(ourQuery, { sort: sort || { _updatedAt: -1 }, @@ -90,6 +87,7 @@ API.v1.addRoute( history, offset, items: history.length, + count: history.length, total, }); }, @@ -131,6 +129,7 @@ API.v1.addRoute( integrations, offset, items: integrations.length, + count: integrations.length, total, }); }, @@ -139,7 +138,7 @@ API.v1.addRoute( API.v1.addRoute( 'integrations.remove', - { authRequired: true }, + { authRequired: true, validateParams: isIntegrationsRemoveProps }, { post() { if ( @@ -153,48 +152,51 @@ API.v1.addRoute( return API.v1.unauthorized(); } - check( - this.bodyParams, - Match.ObjectIncluding({ - type: String, - target_url: Match.Maybe(String), - integrationId: Match.Maybe(String), - }), - ); - - if (!this.bodyParams.target_url && !this.bodyParams.integrationId) { - return API.v1.failure('An integrationId or target_url needs to be provided.'); - } + const { bodyParams } = this; - let integration; - switch (this.bodyParams.type) { + let integration: IIntegration | null = null; + switch (bodyParams.type) { case 'webhook-outgoing': - if (this.bodyParams.target_url) { - integration = Promise.await(Integrations.findOne({ urls: this.bodyParams.target_url })); - } else if (this.bodyParams.integrationId) { - integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); + if (!bodyParams.target_url && !bodyParams.integrationId) { + return API.v1.failure('An integrationId or target_url needs to be provided.'); + } + + if (bodyParams.target_url) { + integration = Promise.await(Integrations.findOne({ urls: bodyParams.target_url })); + } else if (bodyParams.integrationId) { + integration = Promise.await(Integrations.findOne({ _id: bodyParams.integrationId })); } if (!integration) { return API.v1.failure('No integration found.'); } + const outgoingId = integration._id; + Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteOutgoingIntegration', integration._id); + Meteor.call('deleteOutgoingIntegration', outgoingId); }); return API.v1.success({ integration, }); case 'webhook-incoming': - integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); + check( + bodyParams, + Match.ObjectIncluding({ + integrationId: String, + }), + ); + + integration = Promise.await(Integrations.findOne({ _id: bodyParams.integrationId })); if (!integration) { return API.v1.failure('No integration found.'); } + const incomingId = integration._id; Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteIncomingIntegration', integration._id); + Meteor.call('deleteIncomingIntegration', incomingId); }); return API.v1.success({ @@ -209,7 +211,7 @@ API.v1.addRoute( API.v1.addRoute( 'integrations.get', - { authRequired: true }, + { authRequired: true, validateParams: isIntegrationsGetProps }, { get() { const { integrationId, createdBy } = this.queryParams; @@ -232,58 +234,37 @@ API.v1.addRoute( API.v1.addRoute( 'integrations.update', - { authRequired: true }, + { authRequired: true, validateParams: isIntegrationsUpdateProps }, { put() { - check( - this.bodyParams, - Match.ObjectIncluding({ - type: String, - name: String, - enabled: Boolean, - username: String, - urls: Match.Maybe([String]), - channel: String, - event: Match.Maybe(String), - triggerWords: Match.Maybe([String]), - alias: Match.Maybe(String), - avatar: Match.Maybe(String), - emoji: Match.Maybe(String), - token: Match.Maybe(String), - scriptEnabled: Boolean, - script: Match.Maybe(String), - targetChannel: Match.Maybe(String), - integrationId: Match.Maybe(String), - target_url: Match.Maybe(String), - }), - ); + const { bodyParams } = this; let integration; - switch (this.bodyParams.type) { + switch (bodyParams.type) { case 'webhook-outgoing': - if (this.bodyParams.target_url) { - integration = Promise.await(Integrations.findOne({ urls: this.bodyParams.target_url })); - } else if (this.bodyParams.integrationId) { - integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); + if (bodyParams.target_url) { + integration = Promise.await(Integrations.findOne({ urls: bodyParams.target_url })); + } else if (bodyParams.integrationId) { + integration = Promise.await(Integrations.findOne({ _id: bodyParams.integrationId })); } if (!integration) { return API.v1.failure('No integration found.'); } - Meteor.call('updateOutgoingIntegration', integration._id, this.bodyParams); + Meteor.call('updateOutgoingIntegration', integration._id, bodyParams); return API.v1.success({ integration: Promise.await(Integrations.findOne({ _id: integration._id })), }); case 'webhook-incoming': - integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); + integration = Promise.await(Integrations.findOne({ _id: bodyParams.integrationId })); if (!integration) { return API.v1.failure('No integration found.'); } - Meteor.call('updateIncomingIntegration', integration._id, this.bodyParams); + Meteor.call('updateIncomingIntegration', integration._id, bodyParams); return API.v1.success({ integration: Promise.await(Integrations.findOne({ _id: integration._id })), diff --git a/apps/meteor/app/apps/client/@types/IOrchestrator.ts b/apps/meteor/app/apps/client/@types/IOrchestrator.ts new file mode 100644 index 000000000000..f178cd03960d --- /dev/null +++ b/apps/meteor/app/apps/client/@types/IOrchestrator.ts @@ -0,0 +1,204 @@ +import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata/IAppInfo'; +import { ISetting } from '@rocket.chat/apps-engine/definition/settings/ISetting'; + +export interface IDetailedDescription { + raw: string; + rendered: string; +} + +export interface IDetailedChangelog { + raw: string; + rendered: string; +} + +export interface IAuthor { + name: string; + support: string; + homepage: string; +} + +export interface ILicense { + license: string; + version: number; + expireDate: Date; +} + +export interface ISubscriptionInfo { + typeOf: string; + status: string; + statusFromBilling: boolean; + isSeatBased: boolean; + seats: number; + maxSeats: number; + license: ILicense; + startDate: Date; + periodEnd: Date; + endDate: Date; + externallyManaged: boolean; + isSubscribedViaBundle: boolean; +} + +export interface IPermission { + name: string; + scopes: string[]; +} + +export interface ILatest { + internalId: string; + id: string; + name: string; + nameSlug: string; + version: string; + categories: string[]; + description: string; + detailedDescription: IDetailedDescription; + detailedChangelog: IDetailedChangelog; + requiredApiVersion: string; + author: IAuthor; + classFile: string; + iconFile: string; + iconFileData: string; + status: string; + isVisible: boolean; + createdDate: Date; + modifiedDate: Date; + isPurchased: boolean; + isSubscribed: boolean; + subscriptionInfo: ISubscriptionInfo; + compiled: boolean; + compileJobId: string; + changesNote: string; + languages: string[]; + privacyPolicySummary: string; + internalChangesNote: string; + permissions: IPermission[]; +} + +export interface IBundledIn { + bundleId: string; + bundleName: string; + addonTierId: string; +} + +export interface IILicense { + license: string; + version: number; + expireDate: Date; +} + +export interface ITier { + perUnit: boolean; + minimum: number; + maximum: number; + price: number; + refId: string; +} + +export interface IPricingPlan { + id: string; + enabled: boolean; + price: number; + trialDays: number; + strategy: string; + isPerSeat: boolean; + tiers: ITier[]; +} + +export enum EAppPurchaseType { + PurchaseTypeEmpty = '', + PurchaseTypeBuy = 'buy', + PurchaseTypeSubscription = 'subscription', +} + +export interface IAppFromMarketplace { + appId: string; + latest: ILatest; + isAddon: boolean; + isEnterpriseOnly: boolean; + isBundle: boolean; + bundledAppIds: any[]; + bundledIn: IBundledIn[]; + isPurchased: boolean; + isSubscribed: boolean; + subscriptionInfo: ISubscriptionInfo; + price: number; + purchaseType: EAppPurchaseType; + isUsageBased: boolean; + createdAt: Date; + modifiedAt: Date; + pricingPlans: IPricingPlan[]; + addonId: string; +} + +export interface ILanguageInfo { + Params: string; + Description: string; + Setting_Name: string; + Setting_Description: string; +} + +export interface ILanguages { + [key: string]: ILanguageInfo; +} + +export interface IAppLanguage { + id: string; + languages: ILanguages; +} + +export interface IAppExternalURL { + url: string; + success: boolean; +} + +export interface ICategory { + createdDate: Date; + description: string; + id: string; + modifiedDate: Date; + title: string; +} + +export interface IDeletedInstalledApp { + app: IAppInfo; + success: boolean; +} + +export interface IAppSynced { + app: IAppFromMarketplace; + success: boolean; +} + +export interface IScreenshot { + id: string; + appId: string; + fileName: string; + altText: string; + accessUrl: string; + thumbnailUrl: string; + createdAt: Date; + modifiedAt: Date; +} + +export interface IAppScreenshots { + screenshots: IScreenshot[]; + success: boolean; +} + +export interface ISettings { + [key: string]: ISetting; +} + +export interface ISettingsReturn { + settings: ISettings; + success: boolean; +} + +export interface ISettingsPayload { + settings: ISetting[]; +} + +export interface ISettingsSetReturn { + updated: ISettings; + success: boolean; +} diff --git a/apps/meteor/app/apps/client/orchestrator.js b/apps/meteor/app/apps/client/orchestrator.js deleted file mode 100644 index b9194bf5d83e..000000000000 --- a/apps/meteor/app/apps/client/orchestrator.js +++ /dev/null @@ -1,204 +0,0 @@ -import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; - -import { dispatchToastMessage } from '../../../client/lib/toast'; -import { hasAtLeastOnePermission } from '../../authorization'; -import { settings } from '../../settings/client'; -import { CachedCollectionManager } from '../../ui-cached-collection'; -import { APIClient } from '../../utils'; -import { AppWebsocketReceiver } from './communication'; -import { handleI18nResources } from './i18n'; -import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; - -const createDeferredValue = () => { - let resolve; - let reject; - const promise = new Promise((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }); - - return [promise, resolve, reject]; -}; - -class AppClientOrchestrator { - constructor() { - this._appClientUIHost = new RealAppsEngineUIHost(); - this._manager = new AppClientManager(this._appClientUIHost); - this.isLoaded = false; - [this.deferredIsEnabled, this.setEnabled] = createDeferredValue(); - } - - load = async (isEnabled) => { - if (!this.isLoaded) { - this.ws = new AppWebsocketReceiver(); - this.isLoaded = true; - } - - this.setEnabled(isEnabled); - - // 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(); - - await handleI18nResources(); - this.setEnabled(isEnabled); - }; - - getWsListener = () => this.ws; - - getAppClientManager = () => this._manager; - - handleError = (error) => { - console.error(error); - if (hasAtLeastOnePermission(['manage-apps'])) { - dispatchToastMessage({ - type: 'error', - message: error.message, - }); - } - }; - - isEnabled = () => this.deferredIsEnabled; - - 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, isEnterpriseOnly, modifiedAt }) => ({ - ...latest, - price, - pricingPlans, - purchaseType, - isEnterpriseOnly, - modifiedAt, - })); - }; - - 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; - }; - - 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; - }; - - setAppSettings = async (appId, settings) => { - const { updated } = await APIClient.post(`apps/${appId}/settings`, undefined, { settings }); - return updated; - }; - - 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, permissionsGranted) => { - const { app } = await APIClient.post('apps/', { - appId, - marketplace: true, - version, - permissionsGranted, - }); - return app; - }; - - updateApp = async (appId, version, permissionsGranted) => { - const { app } = await APIClient.post(`apps/${appId}`, { - appId, - marketplace: true, - version, - permissionsGranted, - }); - return app; - }; - - uninstallApp = (appId) => APIClient.delete(`apps/${appId}`); - - syncApp = (appId) => APIClient.post(`apps/${appId}/sync`); - - setAppStatus = async (appId, status) => { - const { status: effectiveStatus } = await APIClient.post(`apps/${appId}/status`, { status }); - return effectiveStatus; - }; - - screenshots = (appId) => APIClient.get(`apps/${appId}/screenshots`); - - enableApp = (appId) => this.setAppStatus(appId, 'manually_enabled'); - - disableApp = (appId) => this.setAppStatus(appId, 'manually_disabled'); - - buildExternalUrl = (appId, purchaseType = 'buy', details = false) => - APIClient.get('apps', { - buildExternalUrl: 'true', - appId, - purchaseType, - details, - }); - - getCategories = async () => { - const categories = await APIClient.get('apps', { categories: 'true' }); - return categories; - }; - - getUIHost = () => this._appClientUIHost; -} - -export const Apps = new AppClientOrchestrator(); - -Meteor.startup(() => { - CachedCollectionManager.onLogin(() => { - Meteor.call('apps/is-enabled', (error, isEnabled) => { - if (error) { - Apps.handleError(error); - return; - } - - Apps.getAppClientManager().initialize(); - Apps.load(isEnabled); - }); - }); - - Tracker.autorun(() => { - const isEnabled = settings.get('Apps_Framework_enabled'); - Apps.load(isEnabled); - }); -}); diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts new file mode 100644 index 000000000000..ecbe4c9ed818 --- /dev/null +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -0,0 +1,285 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager'; +import { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; +import { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + +import { App } from '../../../client/views/admin/apps/types'; +import { dispatchToastMessage } from '../../../client/lib/toast'; +import { settings } from '../../settings/client'; +import { CachedCollectionManager } from '../../ui-cached-collection'; +import { createDeferredValue } from '../lib/misc/DeferredValue'; +import { + IPricingPlan, + EAppPurchaseType, + IAppFromMarketplace, + IAppLanguage, + IAppExternalURL, + ICategory, + IDeletedInstalledApp, + IAppSynced, + IAppScreenshots, + IAuthor, + IDetailedChangelog, + IDetailedDescription, + ISubscriptionInfo, + ISettingsReturn, + ISettingsPayload, + ISettingsSetReturn, +} from './@types/IOrchestrator'; +import { AppWebsocketReceiver } from './communication'; +import { handleI18nResources } from './i18n'; +import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; + +const { APIClient } = require('../../utils'); +const { hasAtLeastOnePermission } = require('../../authorization'); + +export interface IAppsFromMarketplace { + price: number; + pricingPlans: IPricingPlan[]; + purchaseType: EAppPurchaseType; + isEnterpriseOnly: boolean; + modifiedAt: Date; + internalId: string; + id: string; + name: string; + nameSlug: string; + version: string; + categories: string[]; + description: string; + detailedDescription: IDetailedDescription; + detailedChangelog: IDetailedChangelog; + requiredApiVersion: string; + author: IAuthor; + classFile: string; + iconFile: string; + iconFileData: string; + status: string; + isVisible: boolean; + createdDate: Date; + modifiedDate: Date; + isPurchased: boolean; + isSubscribed: boolean; + subscriptionInfo: ISubscriptionInfo; + compiled: boolean; + compileJobId: string; + changesNote: string; + languages: string[]; + privacyPolicySummary: string; + internalChangesNote: string; + permissions: IPermission[]; +} + +class AppClientOrchestrator { + private _appClientUIHost: RealAppsEngineUIHost; + + private _manager: AppClientManager; + + private isLoaded: boolean; + + private ws: AppWebsocketReceiver; + + private setEnabled: (value: boolean | PromiseLike) => void; + + private deferredIsEnabled: Promise | undefined; + + constructor() { + this._appClientUIHost = new RealAppsEngineUIHost(); + this._manager = new AppClientManager(this._appClientUIHost); + this.isLoaded = false; + const { promise, resolve } = createDeferredValue(); + this.deferredIsEnabled = promise; + this.setEnabled = resolve; + } + + public async load(isEnabled: boolean): Promise { + if (!this.isLoaded) { + this.ws = new AppWebsocketReceiver(); + this.isLoaded = true; + } + + await handleI18nResources(); + + this.setEnabled(isEnabled); + } + + public getWsListener(): AppWebsocketReceiver { + return this.ws; + } + + public getAppClientManager(): AppClientManager { + return this._manager; + } + + public handleError(error: Error): void { + if (hasAtLeastOnePermission(['manage-apps'])) { + dispatchToastMessage({ + type: 'error', + message: error.message, + }); + } + } + + public screenshots(appId: string): IAppScreenshots { + return APIClient.get(`apps/${appId}/screenshots`); + } + + public isEnabled(): Promise | undefined { + return this.deferredIsEnabled; + } + + public async getApps(): Promise { + const { apps } = await APIClient.get('apps'); + return apps; + } + + public async getAppsFromMarketplace(): Promise { + const appsOverviews: IAppFromMarketplace[] = await APIClient.get('apps', { marketplace: 'true' }); + return appsOverviews.map((app: IAppFromMarketplace) => { + const { latest, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt } = app; + return { + ...latest, + price, + pricingPlans, + purchaseType, + isEnterpriseOnly, + modifiedAt, + }; + }); + } + + public async getAppsOnBundle(bundleId: string): Promise { + const { apps } = await APIClient.get(`apps/bundles/${bundleId}/apps`); + return apps; + } + + public async getAppsLanguages(): Promise { + const { apps } = await APIClient.get('apps/languages'); + return apps; + } + + public async getApp(appId: string): Promise { + const { app } = await APIClient.get(`apps/${appId}`); + return app; + } + + public async getAppFromMarketplace(appId: string, version: string): Promise { + const { app } = await APIClient.get(`apps/${appId}`, { + marketplace: 'true', + version, + }); + return app; + } + + public async getLatestAppFromMarketplace(appId: string, version: string): Promise { + const { app } = await APIClient.get(`apps/${appId}`, { + marketplace: 'true', + update: 'true', + appVersion: version, + }); + return app; + } + + public async getAppSettings(appId: string): Promise { + const { settings } = await APIClient.get(`apps/${appId}/settings`); + return settings; + } + + public async setAppSettings(appId: string, settings: ISettingsPayload): Promise { + const { updated } = await APIClient.post(`apps/${appId}/settings`, undefined, { settings }); + return updated; + } + + public async getAppApis(appId: string): Promise { + const { apis } = await APIClient.get(`apps/${appId}/apis`); + return apis; + } + + public async getAppLanguages(appId: string): Promise { + const { languages } = await APIClient.get(`apps/${appId}/languages`); + return languages; + } + + public async installApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise { + const { app } = await APIClient.post('apps/', { + appId, + marketplace: true, + version, + permissionsGranted, + }); + return app; + } + + public async updateApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise { + const { app } = await APIClient.post(`apps/${appId}`, { + appId, + marketplace: true, + version, + permissionsGranted, + }); + return app; + } + + public uninstallApp(appId: string): IDeletedInstalledApp { + return APIClient.delete(`apps/${appId}`); + } + + public syncApp(appId: string): IAppSynced { + return APIClient.post(`apps/${appId}/sync`); + } + + public async setAppStatus(appId: string, status: AppStatus): Promise { + const { status: effectiveStatus } = await APIClient.post(`apps/${appId}/status`, { status }); + return effectiveStatus; + } + + public enableApp(appId: string): Promise { + return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED); + } + + public disableApp(appId: string): Promise { + return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED); + } + + public buildExternalUrl(appId: string, purchaseType = 'buy', details = false): IAppExternalURL { + return APIClient.get('apps', { + buildExternalUrl: 'true', + appId, + purchaseType, + details, + }); + } + + public async getCategories(): Promise { + const categories = await APIClient.get('apps', { categories: 'true' }); + return categories; + } + + public getUIHost(): RealAppsEngineUIHost { + return this._appClientUIHost; + } +} + +export const Apps = new AppClientOrchestrator(); + +Meteor.startup(() => { + CachedCollectionManager.onLogin(() => { + Meteor.call('apps/is-enabled', (error: Error, isEnabled: boolean) => { + if (error) { + Apps.handleError(error); + return; + } + + Apps.getAppClientManager().initialize(); + Apps.load(isEnabled); + }); + }); + + Tracker.autorun(() => { + const isEnabled = settings.get('Apps_Framework_enabled'); + Apps.load(isEnabled); + }); +}); diff --git a/apps/meteor/app/apps/lib/misc/DeferredValue.ts b/apps/meteor/app/apps/lib/misc/DeferredValue.ts new file mode 100644 index 000000000000..6089920024c1 --- /dev/null +++ b/apps/meteor/app/apps/lib/misc/DeferredValue.ts @@ -0,0 +1,33 @@ +export type ResolveHandler = (value: T | PromiseLike) => void; +export type RejectHandler = (reason: unknown) => void; + +class Deferred { + promise: Promise; + + resolve!: ResolveHandler; + + reject!: RejectHandler; + + constructor() { + this.promise = new Promise((_resolve, _reject) => { + this.resolve = _resolve; + this.reject = _reject; + }); + } + + get computedPromise(): Promise { + return this.promise; + } + + get computedResolve(): ResolveHandler { + return this.resolve; + } + + get computedReject(): RejectHandler { + return this.reject; + } +} + +const createDeferredValue = (): Deferred => new Deferred(); + +export { createDeferredValue }; diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index 3528aeebe588..514ca2ff6c39 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -43,6 +43,10 @@ export class AppListenerBridge { case AppInterface.IPostLivechatGuestSaved: case AppInterface.IPostLivechatRoomSaved: return 'livechatEvent'; + case AppInterface.IPostUserCreated: + case AppInterface.IPostUserUpdated: + case AppInterface.IPostUserDeleted: + return 'userEvent'; default: return 'defaultEvent'; } @@ -134,4 +138,17 @@ export class AppListenerBridge { return this.orch.getManager().getListenerManager().executeListener(inte, room); } } + + async userEvent(inte, data) { + const context = { + user: this.orch.getConverters().get('users').convertToApp(data.user), + performedBy: this.orch.getConverters().get('users').convertToApp(data.performedBy), + }; + + if (inte === AppInterface.IPostUserUpdated) { + context.previousData = this.orch.getConverters().get('users').convertToApp(data.previousUser); + } + + return this.orch.getManager().getListenerManager().executeListener(inte, context); + } } diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 557c6454fee0..b4d58c2fe572 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -17,6 +17,7 @@ import { isValidAttemptByUser, isValidLoginAttemptByIp } from '../lib/restrictLo import './settings'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { getNewUserRoles } from '../../../../server/services/user/lib/getNewUserRoles'; +import { AppEvents, Apps } from '../../../apps/server/orchestrator'; Accounts.config({ forbidClientAccountCreation: true, @@ -210,6 +211,10 @@ Accounts.onCreateUser(function (options, user = {}) { } callbacks.run('onCreateUser', options, user); + + // App IPostUserCreated event hook + Promise.await(Apps.triggerEvent(AppEvents.IPostUserCreated, { user, performedBy: Meteor.user() })); + return user; }); diff --git a/apps/meteor/app/autotranslate/client/index.js b/apps/meteor/app/autotranslate/client/index.ts similarity index 100% rename from apps/meteor/app/autotranslate/client/index.js rename to apps/meteor/app/autotranslate/client/index.ts diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.js b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts similarity index 74% rename from apps/meteor/app/autotranslate/client/lib/autotranslate.js rename to apps/meteor/app/autotranslate/client/lib/autotranslate.ts index 44ef5ae7953b..d7f8338de98f 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.js +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; import mem from 'mem'; +import { IRoom, ISubscription, ISupportedLanguage, ITranslatedMessage, IUser, MessageAttachmentDefault } from '@rocket.chat/core-typings'; -import { Subscriptions, Messages } from '../../../models'; -import { hasPermission } from '../../../authorization'; +import { Subscriptions, Messages } from '../../../models/client'; +import { hasPermission } from '../../../authorization/client'; import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; let userLanguage = 'en'; @@ -12,29 +13,29 @@ let username = ''; Meteor.startup(() => { Tracker.autorun(() => { - const user = Meteor.user(); + const user: Pick | null = Meteor.user(); if (!user) { return; } userLanguage = user.language || 'en'; - username = user.username; + username = user.username || ''; }); }); export const AutoTranslate = { initialized: false, providersMetadata: {}, - messageIdsToWait: {}, - supportedLanguages: [], + messageIdsToWait: {} as { [messageId: string]: string }, + supportedLanguages: [] as ISupportedLanguage[], findSubscriptionByRid: mem((rid) => Subscriptions.findOne({ rid })), - getLanguage(rid) { - let subscription = {}; + getLanguage(rid: IRoom['_id']): string { + let subscription: ISubscription | undefined; if (rid) { subscription = this.findSubscriptionByRid(rid); } - const language = (subscription && subscription.autoTranslateLanguage) || userLanguage || window.defaultUserLanguage(); + const language = (subscription?.autoTranslateLanguage || userLanguage || window.defaultUserLanguage?.()) as string; if (language.indexOf('-') !== -1) { if (!_.findWhere(this.supportedLanguages, { language })) { return language.substr(0, 2); @@ -43,7 +44,7 @@ export const AutoTranslate = { return language; }, - translateAttachments(attachments, language) { + translateAttachments(attachments: MessageAttachmentDefault[], language: string): MessageAttachmentDefault[] { for (const attachment of attachments) { if (attachment.author_name !== username) { if (attachment.text && attachment.translations && attachment.translations[language]) { @@ -54,7 +55,9 @@ export const AutoTranslate = { attachment.description = attachment.translations[language]; } + // @ts-expect-error - not sure what to do with this if (attachment.attachments && attachment.attachments.length > 0) { + // @ts-expect-error - not sure what to do with this attachment.attachments = this.translateAttachments(attachment.attachments, language); } } @@ -62,7 +65,7 @@ export const AutoTranslate = { return attachments; }, - init() { + init(): void { if (this.initialized) { return; } @@ -82,7 +85,7 @@ export const AutoTranslate = { }); Subscriptions.find().observeChanges({ - changed: (id, fields) => { + changed: (_id: string, fields: ISubscription) => { if (fields.hasOwnProperty('autoTranslate') || fields.hasOwnProperty('autoTranslateLanguage')) { mem.clear(this.findSubscriptionByRid); } @@ -93,17 +96,17 @@ export const AutoTranslate = { }, }; -export const createAutoTranslateMessageRenderer = () => { +export const createAutoTranslateMessageRenderer = (): ((message: ITranslatedMessage) => ITranslatedMessage) => { AutoTranslate.init(); - return (message) => { + return (message: ITranslatedMessage): ITranslatedMessage => { const subscription = AutoTranslate.findSubscriptionByRid(message.rid); const autoTranslateLanguage = AutoTranslate.getLanguage(message.rid); if (message.u && message.u._id !== Meteor.userId()) { if (!message.translations) { message.translations = {}; } - if (!!(subscription && subscription.autoTranslate) !== !!message.autoTranslateShowInverse) { + if (!!subscription?.autoTranslate !== !!message.autoTranslateShowInverse) { message.translations.original = message.html; if (message.translations[autoTranslateLanguage]) { message.html = message.translations[autoTranslateLanguage]; @@ -120,10 +123,10 @@ export const createAutoTranslateMessageRenderer = () => { }; }; -export const createAutoTranslateMessageStreamHandler = () => { +export const createAutoTranslateMessageStreamHandler = (): ((message: ITranslatedMessage) => void) => { AutoTranslate.init(); - return (message) => { + return (message: ITranslatedMessage): void => { if (message.u && message.u._id !== Meteor.userId()) { const subscription = AutoTranslate.findSubscriptionByRid(message.rid); const language = AutoTranslate.getLanguage(message.rid); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.js b/apps/meteor/app/autotranslate/server/autotranslate.ts similarity index 66% rename from apps/meteor/app/autotranslate/server/autotranslate.js rename to apps/meteor/app/autotranslate/server/autotranslate.ts index a41837292af6..a5f550d777d6 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.js +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -1,12 +1,23 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { escapeHTML } from '@rocket.chat/string-helpers'; +import { + IMessage, + IRoom, + MessageAttachment, + ISupportedLanguages, + IProviderMetadata, + ISupportedLanguage, + ITranslationResult, +} from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { callbacks } from '../../../lib/callbacks'; -import { Subscriptions, Messages } from '../../models'; +import { Subscriptions, Messages } from '../../models/server'; import { Markdown } from '../../markdown/server'; -import { Logger } from '../../logger'; +import { Logger } from '../../logger/server'; + +const translationLogger = new Logger('AutoTranslate'); const Providers = Symbol('Providers'); const Provider = Symbol('Provider'); @@ -16,44 +27,57 @@ const Provider = Symbol('Provider'); * register,load and also returns the active provider. */ export class TranslationProviderRegistry { - static [Providers] = {}; + static [Providers]: { [k: string]: AutoTranslate } = {}; static enabled = false; - static [Provider] = null; + static [Provider]: string | null = null; /** * Registers the translation provider into the registry. * @param {*} provider */ - static registerProvider(provider) { + static registerProvider(provider: AutoTranslate): void { // get provider information const metadata = provider._getProviderMetadata(); + if (!metadata) { + translationLogger.error('Provider metadata is not defined'); + return; + } + TranslationProviderRegistry[Providers][metadata.name] = provider; } /** * Return the active Translation provider */ - static getActiveProvider() { - return TranslationProviderRegistry.enabled ? TranslationProviderRegistry[Providers][TranslationProviderRegistry[Provider]] : undefined; + static getActiveProvider(): AutoTranslate | null { + if (!TranslationProviderRegistry.enabled) { + return null; + } + const provider = TranslationProviderRegistry[Provider]; + if (!provider) { + return null; + } + + return TranslationProviderRegistry[Providers][provider]; } - static getSupportedLanguages(...args) { - return TranslationProviderRegistry.enabled - ? TranslationProviderRegistry.getActiveProvider()?.getSupportedLanguages(...args) - : undefined; + static getSupportedLanguages(target: string): ISupportedLanguage[] | undefined { + return TranslationProviderRegistry.enabled ? TranslationProviderRegistry.getActiveProvider()?.getSupportedLanguages(target) : undefined; } - static translateMessage(...args) { - return TranslationProviderRegistry.enabled ? TranslationProviderRegistry.getActiveProvider()?.translateMessage(...args) : undefined; + static translateMessage(message: IMessage, room: IRoom, targetLanguage: string): IMessage | undefined { + return TranslationProviderRegistry.enabled + ? TranslationProviderRegistry.getActiveProvider()?.translateMessage(message, room, targetLanguage) + : undefined; } - static getProviders() { + static getProviders(): AutoTranslate[] { return Object.values(TranslationProviderRegistry[Providers]); } - static setCurrentProvider(provider) { + static setCurrentProvider(provider: string): void { if (provider === TranslationProviderRegistry[Provider]) { return; } @@ -63,13 +87,13 @@ export class TranslationProviderRegistry { TranslationProviderRegistry.registerCallbacks(); } - static setEnable(enabled) { + static setEnable(enabled: boolean): void { TranslationProviderRegistry.enabled = enabled; TranslationProviderRegistry.registerCallbacks(); } - static registerCallbacks() { + static registerCallbacks(): void { if (!TranslationProviderRegistry.enabled) { callbacks.remove('afterSaveMessage', 'autotranslate'); return; @@ -91,7 +115,13 @@ export class TranslationProviderRegistry { * @abstract * @class */ -export class AutoTranslate { +export abstract class AutoTranslate { + name: string; + + languages: string[]; + + supportedLanguages: ISupportedLanguages; + /** * Encapsulate the api key and provider settings. * @constructor @@ -107,7 +137,7 @@ export class AutoTranslate { * @param {object} message * @return {object} message */ - tokenize(message) { + tokenize(message: IMessage): IMessage { if (!message.tokens || !Array.isArray(message.tokens)) { message.tokens = []; } @@ -118,11 +148,11 @@ export class AutoTranslate { return message; } - tokenizeEmojis(message) { - let count = message.tokens.length; + tokenizeEmojis(message: IMessage): IMessage { + let count = message.tokens?.length || 0; message.msg = message.msg.replace(/:[+\w\d]+:/g, function (match) { const token = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token, text: match, }); @@ -132,23 +162,23 @@ export class AutoTranslate { return message; } - tokenizeURLs(message) { - let count = message.tokens.length; + tokenizeURLs(message: IMessage): IMessage { + let count = message.tokens?.length || 0; - const schemes = settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); + const schemes = settings.get('Markdown_SupportSchemesForLink')?.split(',').join('|'); // Support ![alt text](http://image url) and [text](http://link) message.msg = message.msg.replace( new RegExp(`(!?\\[)([^\\]]+)(\\]\\((?:${schemes}):\\/\\/[^\\)]+\\))`, 'gm'), - function (match, pre, text, post) { + function (_match, pre, text, post) { const pretoken = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token: pretoken, text: pre, }); const posttoken = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token: posttoken, text: post, }); @@ -160,15 +190,15 @@ export class AutoTranslate { // Support message.msg = message.msg.replace( new RegExp(`((?:<|<)(?:${schemes}):\\/\\/[^\\|]+\\|)(.+?)(?=>|>)((?:>|>))`, 'gm'), - function (match, pre, text, post) { + function (_match, pre, text, post) { const pretoken = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token: pretoken, text: pre, }); const posttoken = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token: posttoken, text: post, }); @@ -180,8 +210,8 @@ export class AutoTranslate { return message; } - tokenizeCode(message) { - let count = message.tokens.length; + tokenizeCode(message: IMessage): IMessage { + let count = message.tokens?.length || 0; message.html = message.msg; message = Markdown.parseMessageNotEscaped(message); @@ -189,28 +219,26 @@ export class AutoTranslate { const regexWrappedParagraph = new RegExp('^\\s*

|

\\s*$', 'gm'); message.msg = message.msg.replace(regexWrappedParagraph, ''); - for (const tokenIndex in message.tokens) { - if (message.tokens.hasOwnProperty(tokenIndex)) { - const { token } = message.tokens[tokenIndex]; - if (token.indexOf('notranslate') === -1) { - const newToken = `{${count++}}`; - message.msg = message.msg.replace(token, newToken); - message.tokens[tokenIndex].token = newToken; - } + for (const [tokenIndex, value] of message.tokens?.entries() ?? []) { + const { token } = value; + if (token.indexOf('notranslate') === -1) { + const newToken = `{${count++}}`; + message.msg = message.msg.replace(token, newToken); + message.tokens ? (message.tokens[tokenIndex].token = newToken) : undefined; } } return message; } - tokenizeMentions(message) { - let count = message.tokens.length; + tokenizeMentions(message: IMessage): IMessage { + let count = message.tokens?.length || 0; if (message.mentions && message.mentions.length > 0) { message.mentions.forEach((mention) => { message.msg = message.msg.replace(new RegExp(`(@${mention.username})`, 'gm'), (match) => { const token = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token, text: match, }); @@ -223,7 +251,7 @@ export class AutoTranslate { message.channels.forEach((channel) => { message.msg = message.msg.replace(new RegExp(`(#${channel.name})`, 'gm'), (match) => { const token = `{${count++}}`; - message.tokens.push({ + message.tokens?.push({ token, text: match, }); @@ -235,8 +263,8 @@ export class AutoTranslate { return message; } - deTokenize(message) { - if (message.tokens && message.tokens.length > 0) { + deTokenize(message: IMessage): string { + if (message.tokens && message.tokens?.length > 0) { for (const { token, text, noHtml } of message.tokens) { message.msg = message.msg.replace(token, () => noHtml || text); } @@ -253,8 +281,8 @@ export class AutoTranslate { * @param {object} targetLanguage * @returns {object} unmodified message object. */ - translateMessage(message, room, targetLanguage) { - let targetLanguages; + translateMessage(message: IMessage, room: IRoom, targetLanguage: string): IMessage { + let targetLanguages: string[]; if (targetLanguage) { targetLanguages = [targetLanguage]; } else { @@ -275,14 +303,11 @@ export class AutoTranslate { if (message.attachments && message.attachments.length > 0) { Meteor.defer(() => { - for (const index in message.attachments) { - if (message.attachments.hasOwnProperty(index)) { - const attachment = message.attachments[index]; - if (attachment.description || attachment.text) { - const translations = this._translateAttachmentDescriptions(attachment, targetLanguages); - if (!_.isEmpty(translations)) { - Messages.addAttachmentTranslations(message._id, index, translations); - } + for (const [index, attachment] of message.attachments?.entries() ?? []) { + if (attachment.description || attachment.text) { + const translations = this._translateAttachmentDescriptions(attachment, targetLanguages); + if (!_.isEmpty(translations)) { + Messages.addAttachmentTranslations(message._id, index, translations); } } } @@ -299,9 +324,7 @@ export class AutoTranslate { * @returns { name, displayName, settings } }; */ - _getProviderMetadata() { - Logger.warn('must be implemented by subclass!', '_getProviderMetadata'); - } + abstract _getProviderMetadata(): IProviderMetadata; /** * Provides the possible languages _from_ which a message can be translated into a target language @@ -310,9 +333,7 @@ export class AutoTranslate { * @param {string} target - the language into which shall be translated * @returns [{ language, name }] */ - getSupportedLanguages(target) { - Logger.warn('must be implemented by subclass!', 'getSupportedLanguages', target); - } + abstract getSupportedLanguages(target: string): ISupportedLanguage[]; /** * Performs the actual translation of a message, @@ -323,9 +344,7 @@ export class AutoTranslate { * @param {object} targetLanguages * @return {object} */ - _translateMessage(message, targetLanguages) { - Logger.warn('must be implemented by subclass!', '_translateMessage', message, targetLanguages); - } + abstract _translateMessage(message: IMessage, targetLanguages: string[]): ITranslationResult; /** * Performs the actual translation of an attachment (precisely its description), @@ -335,9 +354,7 @@ export class AutoTranslate { * @param {object} targetLanguages * @returns {object} translated messages for each target language */ - _translateAttachmentDescriptions(attachment, targetLanguages) { - Logger.warn('must be implemented by subclass!', '_translateAttachmentDescriptions', attachment, targetLanguages); - } + abstract _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): ITranslationResult; } Meteor.startup(() => { @@ -345,12 +362,12 @@ Meteor.startup(() => { * So the registered provider will be invoked when a message is saved. * All the other inactive service provider must be deactivated. */ - settings.watch('AutoTranslate_ServiceProvider', (providerName) => { + settings.watch('AutoTranslate_ServiceProvider', (providerName) => { TranslationProviderRegistry.setCurrentProvider(providerName); }); // Get Auto Translate Active flag - settings.watch('AutoTranslate_Enabled', (value) => { + settings.watch('AutoTranslate_Enabled', (value) => { TranslationProviderRegistry.setEnable(value); }); }); diff --git a/apps/meteor/app/autotranslate/server/deeplTranslate.js b/apps/meteor/app/autotranslate/server/deeplTranslate.ts similarity index 82% rename from apps/meteor/app/autotranslate/server/deeplTranslate.js rename to apps/meteor/app/autotranslate/server/deeplTranslate.ts index dff3464298ce..4da7b27f8f37 100644 --- a/apps/meteor/app/autotranslate/server/deeplTranslate.js +++ b/apps/meteor/app/autotranslate/server/deeplTranslate.ts @@ -5,10 +5,18 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { HTTP } from 'meteor/http'; import _ from 'underscore'; +import { + IMessage, + IDeepLTranslation, + MessageAttachment, + IProviderMetadata, + ITranslationResult, + ISupportedLanguage, +} from '@rocket.chat/core-typings'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; import { SystemLogger } from '../../../server/lib/logger/system'; -import { settings } from '../../settings'; +import { settings } from '../../settings/server'; /** * DeepL translation service provider class representation. @@ -19,6 +27,10 @@ import { settings } from '../../settings'; * @augments AutoTranslate */ class DeeplAutoTranslate extends AutoTranslate { + apiKey: string; + + apiEndPointUrl: string; + /** * setup api reference to deepl translate to be used as message translation provider. * @constructor @@ -28,7 +40,7 @@ class DeeplAutoTranslate extends AutoTranslate { this.name = 'deepl-translate'; this.apiEndPointUrl = 'https://api.deepl.com/v2/translate'; // Get the service provide API key. - settings.watch('AutoTranslate_DeepLAPIKey', (value) => { + settings.watch('AutoTranslate_DeepLAPIKey', (value) => { this.apiKey = value; }); } @@ -38,7 +50,7 @@ class DeeplAutoTranslate extends AutoTranslate { * @private implements super abstract method. * @return {object} */ - _getProviderMetadata() { + _getProviderMetadata(): IProviderMetadata { return { name: this.name, displayName: TAPi18n.__('AutoTranslate_DeepL'), @@ -51,7 +63,7 @@ class DeeplAutoTranslate extends AutoTranslate { * @private implements super abstract method. * @return {object} */ - _getSettings() { + _getSettings(): IProviderMetadata['settings'] { return { apiKey: this.apiKey, apiEndPointUrl: this.apiEndPointUrl, @@ -66,9 +78,9 @@ class DeeplAutoTranslate extends AutoTranslate { * @param {string} target * @returns {object} code : value pair */ - getSupportedLanguages(target) { + getSupportedLanguages(target: string): ISupportedLanguage[] { if (!this.apiKey) { - return; + return []; } if (this.supportedLanguages[target]) { @@ -184,8 +196,8 @@ class DeeplAutoTranslate extends AutoTranslate { * @param {object} targetLanguages * @returns {object} translations: Translated messages for each language */ - _translateMessage(message, targetLanguages) { - const translations = {}; + _translateMessage(message: IMessage, targetLanguages: string[]): ITranslationResult { + const translations: { [k: string]: string } = {}; let msgs = message.msg.split('\n'); msgs = msgs.map((msg) => encodeURIComponent(msg)); const query = `text=${msgs.join('&text=')}`; @@ -197,7 +209,9 @@ class DeeplAutoTranslate extends AutoTranslate { try { const result = HTTP.get(this.apiEndPointUrl, { params: { + // eslint-disable-next-line @typescript-eslint/camelcase auth_key: this.apiKey, + // eslint-disable-next-line @typescript-eslint/camelcase target_lang: language, }, query, @@ -213,7 +227,9 @@ class DeeplAutoTranslate extends AutoTranslate { // 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])) + .map((translation: IDeepLTranslation, index: number) => + translation.detected_source_language !== language ? translation.text : msgs[index], + ) .join('\n'); translations[language] = this.deTokenize(Object.assign({}, message, { msg: translatedText })); } @@ -231,9 +247,9 @@ class DeeplAutoTranslate extends AutoTranslate { * @param {object} targetLanguages * @returns {object} translated messages for each target language */ - _translateAttachmentDescriptions(attachment, targetLanguages) { - const translations = {}; - const query = `text=${encodeURIComponent(attachment.description || attachment.text)}`; + _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): ITranslationResult { + const translations: { [k: string]: string } = {}; + const query = `text=${encodeURIComponent(attachment.description || attachment.text || '')}`; const supportedLanguages = this.getSupportedLanguages('en'); targetLanguages.forEach((language) => { if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { @@ -242,7 +258,9 @@ class DeeplAutoTranslate extends AutoTranslate { try { const result = HTTP.get(this.apiEndPointUrl, { params: { + // eslint-disable-next-line @typescript-eslint/camelcase auth_key: this.apiKey, + // eslint-disable-next-line @typescript-eslint/camelcase target_lang: language, }, query, @@ -254,8 +272,8 @@ class DeeplAutoTranslate extends AutoTranslate { 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); + if (result.data.translations.map((translation: IDeepLTranslation) => translation.detected_source_language).join() !== language) { + translations[language] = result.data.translations.map((translation: IDeepLTranslation) => translation.text); } } } catch (e) { diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.js b/apps/meteor/app/autotranslate/server/googleTranslate.ts similarity index 82% rename from apps/meteor/app/autotranslate/server/googleTranslate.js rename to apps/meteor/app/autotranslate/server/googleTranslate.ts index af351bcaba12..13a861532e38 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.js +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -5,6 +5,14 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { HTTP } from 'meteor/http'; import _ from 'underscore'; +import { + IMessage, + IProviderMetadata, + ISupportedLanguage, + ITranslationResult, + IGoogleTranslation, + MessageAttachment, +} from '@rocket.chat/core-typings'; import { AutoTranslate, TranslationProviderRegistry } from './autotranslate'; import { SystemLogger } from '../../../server/lib/logger/system'; @@ -17,6 +25,10 @@ import { settings } from '../../settings/server'; */ class GoogleAutoTranslate extends AutoTranslate { + apiKey: string; + + apiEndPointUrl: string; + /** * setup api reference to Google translate to be used as message translation provider. * @constructor @@ -26,7 +38,7 @@ class GoogleAutoTranslate extends AutoTranslate { this.name = 'google-translate'; this.apiEndPointUrl = 'https://translation.googleapis.com/language/translate/v2'; // Get the service provide API key. - settings.watch('AutoTranslate_GoogleAPIKey', (value) => { + settings.watch('AutoTranslate_GoogleAPIKey', (value) => { this.apiKey = value; }); } @@ -36,7 +48,7 @@ class GoogleAutoTranslate extends AutoTranslate { * @private implements super abstract method. * @returns {object} */ - _getProviderMetadata() { + _getProviderMetadata(): IProviderMetadata { return { name: this.name, displayName: TAPi18n.__('AutoTranslate_Google'), @@ -49,7 +61,7 @@ class GoogleAutoTranslate extends AutoTranslate { * @private implements super abstract method. * @returns {object} */ - _getSettings() { + _getSettings(): IProviderMetadata['settings'] { return { apiKey: this.apiKey, apiEndPointUrl: this.apiEndPointUrl, @@ -63,7 +75,7 @@ class GoogleAutoTranslate extends AutoTranslate { * @param {string} target : user language setting or 'en' * @returns {object} code : value pair */ - getSupportedLanguages(target) { + getSupportedLanguages(target: string): ISupportedLanguage[] { if (!this.apiKey) { return []; } @@ -75,12 +87,9 @@ class GoogleAutoTranslate extends AutoTranslate { let result; const params = { key: this.apiKey, + ...(target && { target }), }; - if (target) { - params.target = target; - } - try { result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params, @@ -119,8 +128,8 @@ class GoogleAutoTranslate extends AutoTranslate { * @param {object} targetLanguages * @returns {object} translations: Translated messages for each language */ - _translateMessage(message, targetLanguages) { - const translations = {}; + _translateMessage(message: IMessage, targetLanguages: string[]): ITranslationResult { + const translations: { [k: string]: string } = {}; let msgs = message.msg.split('\n'); msgs = msgs.map((msg) => encodeURIComponent(msg)); @@ -149,7 +158,7 @@ class GoogleAutoTranslate extends AutoTranslate { Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0 ) { - const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n'); + const txt = result.data.data.translations.map((translation: IGoogleTranslation) => translation.translatedText).join('\n'); translations[language] = this.deTokenize(Object.assign({}, message, { msg: txt })); } } catch (e) { @@ -166,9 +175,9 @@ class GoogleAutoTranslate extends AutoTranslate { * @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)}`; + _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): ITranslationResult { + const translations: { [k: string]: string } = {}; + const query = `q=${encodeURIComponent(attachment.description || attachment.text || '')}`; const supportedLanguages = this.getSupportedLanguages('en'); targetLanguages.forEach((language) => { @@ -193,7 +202,9 @@ class GoogleAutoTranslate extends AutoTranslate { Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0 ) { - translations[language] = result.data.data.translations.map((translation) => translation.translatedText).join('\n'); + translations[language] = result.data.data.translations + .map((translation: IGoogleTranslation) => translation.translatedText) + .join('\n'); } } catch (e) { SystemLogger.error('Error translating message', e); diff --git a/apps/meteor/app/autotranslate/server/index.js b/apps/meteor/app/autotranslate/server/index.ts similarity index 78% rename from apps/meteor/app/autotranslate/server/index.js rename to apps/meteor/app/autotranslate/server/index.ts index 8a84b619d582..ea971fe59ab1 100644 --- a/apps/meteor/app/autotranslate/server/index.js +++ b/apps/meteor/app/autotranslate/server/index.ts @@ -11,9 +11,9 @@ import './autotranslate'; import './methods/getSupportedLanguages'; import './methods/saveSettings'; import './methods/translateMessage'; -import './googleTranslate.js'; -import './deeplTranslate.js'; -import './msTranslate.js'; -import './methods/getProviderUiMetadata.js'; +import './googleTranslate'; +import './deeplTranslate'; +import './msTranslate'; +import './methods/getProviderUiMetadata'; export { AutoTranslate, TranslationProviderRegistry }; diff --git a/apps/meteor/app/autotranslate/server/logger.js b/apps/meteor/app/autotranslate/server/logger.ts similarity index 100% rename from apps/meteor/app/autotranslate/server/logger.js rename to apps/meteor/app/autotranslate/server/logger.ts diff --git a/apps/meteor/app/autotranslate/server/methods/getProviderUiMetadata.js b/apps/meteor/app/autotranslate/server/methods/getProviderUiMetadata.ts similarity index 100% rename from apps/meteor/app/autotranslate/server/methods/getProviderUiMetadata.js rename to apps/meteor/app/autotranslate/server/methods/getProviderUiMetadata.ts diff --git a/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.js b/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.ts similarity index 68% rename from apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.js rename to apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.ts index 64ba301efdfd..c74dea902a6e 100644 --- a/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.js +++ b/apps/meteor/app/autotranslate/server/methods/getSupportedLanguages.ts @@ -1,12 +1,19 @@ import { Meteor } from 'meteor/meteor'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; -import { hasPermission } from '../../../authorization'; +import { hasPermission } from '../../../authorization/server'; import { TranslationProviderRegistry } from '..'; Meteor.methods({ 'autoTranslate.getSupportedLanguages'(targetLanguage) { - if (!hasPermission(Meteor.userId(), 'auto-translate')) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'getSupportedLanguages', + }); + } + + if (!hasPermission(userId, 'auto-translate')) { throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { method: 'autoTranslate.saveSettings', }); diff --git a/apps/meteor/app/autotranslate/server/methods/saveSettings.js b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts similarity index 85% rename from apps/meteor/app/autotranslate/server/methods/saveSettings.js rename to apps/meteor/app/autotranslate/server/methods/saveSettings.ts index 9587e890e4ac..601a1273af82 100644 --- a/apps/meteor/app/autotranslate/server/methods/saveSettings.js +++ b/apps/meteor/app/autotranslate/server/methods/saveSettings.ts @@ -1,18 +1,19 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { hasPermission } from '../../../authorization'; -import { Subscriptions } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { Subscriptions } from '../../../models/server'; Meteor.methods({ 'autoTranslate.saveSettings'(rid, field, value, options) { - if (!Meteor.userId()) { + const userId = Meteor.userId(); + if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'saveAutoTranslateSettings', }); } - if (!hasPermission(Meteor.userId(), 'auto-translate')) { + if (!hasPermission(userId, 'auto-translate')) { throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { method: 'autoTranslate.saveSettings', }); @@ -28,7 +29,7 @@ Meteor.methods({ }); } - const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, Meteor.userId()); + const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, userId); if (!subscription) { throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'saveAutoTranslateSettings', diff --git a/apps/meteor/app/autotranslate/server/methods/translateMessage.js b/apps/meteor/app/autotranslate/server/methods/translateMessage.ts similarity index 77% rename from apps/meteor/app/autotranslate/server/methods/translateMessage.js rename to apps/meteor/app/autotranslate/server/methods/translateMessage.ts index bedf65518326..8e41604849bf 100644 --- a/apps/meteor/app/autotranslate/server/methods/translateMessage.js +++ b/apps/meteor/app/autotranslate/server/methods/translateMessage.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Rooms } from '../../../models'; +import { Rooms } from '../../../models/server'; import { TranslationProviderRegistry } from '..'; Meteor.methods({ @@ -8,7 +8,7 @@ Meteor.methods({ if (!TranslationProviderRegistry.enabled) { return; } - const room = Rooms.findOneById(message && message.rid); + const room = Rooms.findOneById(message?.rid); if (message && room) { TranslationProviderRegistry.translateMessage(message, room, targetLanguage); } diff --git a/apps/meteor/app/autotranslate/server/msTranslate.js b/apps/meteor/app/autotranslate/server/msTranslate.ts similarity index 81% rename from apps/meteor/app/autotranslate/server/msTranslate.js rename to apps/meteor/app/autotranslate/server/msTranslate.ts index 4ee381cc938b..ccf34ddaca69 100644 --- a/apps/meteor/app/autotranslate/server/msTranslate.js +++ b/apps/meteor/app/autotranslate/server/msTranslate.ts @@ -5,6 +5,7 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { HTTP } from 'meteor/http'; import _ from 'underscore'; +import { IMessage, IProviderMetadata, ISupportedLanguage, ITranslationResult, MessageAttachment } from '@rocket.chat/core-typings'; import { TranslationProviderRegistry, AutoTranslate } from './autotranslate'; import { msLogger } from './logger'; @@ -19,6 +20,16 @@ import { settings } from '../../settings/server'; * @augments AutoTranslate */ class MsAutoTranslate extends AutoTranslate { + apiKey: string; + + apiEndPointUrl: string; + + apiDetectText: string; + + apiGetLanguages: string; + + breakSentence: string; + /** * setup api reference to Microsoft translate to be used as message translation provider. * @constructor @@ -31,7 +42,7 @@ class MsAutoTranslate extends AutoTranslate { this.apiGetLanguages = 'https://api.cognitive.microsofttranslator.com/languages?api-version=3.0'; this.breakSentence = 'https://api.cognitive.microsofttranslator.com/breaksentence?api-version=3.0'; // Get the service provide API key. - settings.watch('AutoTranslate_MicrosoftAPIKey', (value) => { + settings.watch('AutoTranslate_MicrosoftAPIKey', (value) => { this.apiKey = value; }); } @@ -41,7 +52,7 @@ class MsAutoTranslate extends AutoTranslate { * @private implements super abstract method. * @return {object} */ - _getProviderMetadata() { + _getProviderMetadata(): IProviderMetadata { return { name: this.name, displayName: TAPi18n.__('AutoTranslate_Microsoft'), @@ -54,7 +65,7 @@ class MsAutoTranslate extends AutoTranslate { * @private implements super abstract method. * @return {object} */ - _getSettings() { + _getSettings(): IProviderMetadata['settings'] { return { apiKey: this.apiKey, apiEndPointUrl: this.apiEndPointUrl, @@ -69,9 +80,9 @@ class MsAutoTranslate extends AutoTranslate { * @param {string} target * @returns {object} code : value pair */ - getSupportedLanguages(target) { + getSupportedLanguages(target: string): ISupportedLanguage[] { if (!this.apiKey) { - return; + return []; } if (this.supportedLanguages[target]) { return this.supportedLanguages[target]; @@ -92,8 +103,13 @@ class MsAutoTranslate extends AutoTranslate { * @throws Communication Errors * @returns {object} translations: Translated messages for each language */ - _translate(data, targetLanguages) { - let translations = {}; + _translate( + data: { + Text: string; + }[], + targetLanguages: string[], + ): ITranslationResult { + let translations: { [k: string]: string } = {}; const supportedLanguages = this.getSupportedLanguages('en'); targetLanguages = targetLanguages.map((language) => { if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { @@ -115,7 +131,12 @@ class MsAutoTranslate extends AutoTranslate { translations = Object.assign( {}, ...targetLanguages.map((language) => ({ - [language]: result.data.map((line) => line.translations.find((translation) => translation.to === language).text).join('\n'), + [language]: result.data + .map( + (line: { translations: { to: string; text: string }[] }) => + line.translations.find((translation) => translation.to === language)?.text, + ) + .join('\n'), })), ); } @@ -130,7 +151,7 @@ class MsAutoTranslate extends AutoTranslate { * @param {object} targetLanguages * @returns {object} translations: Translated messages for each language */ - _translateMessage(message, targetLanguages) { + _translateMessage(message: IMessage, targetLanguages: string[]): ITranslationResult { // There are multi-sentence-messages where multiple sentences come from different languages // This is a problem for translation services since the language detection fails. // Thus, we'll split the message in sentences, get them translated, and join them again after translation @@ -150,12 +171,12 @@ class MsAutoTranslate extends AutoTranslate { * @param {object} targetLanguages * @returns {object} translated messages for each target language */ - _translateAttachmentDescriptions(attachment, targetLanguages) { + _translateAttachmentDescriptions(attachment: MessageAttachment, targetLanguages: string[]): ITranslationResult { try { return this._translate( [ { - Text: attachment.description || attachment.text, + Text: attachment.description || attachment.text || '', }, ], targetLanguages, diff --git a/apps/meteor/app/integrations/lib/outgoingEvents.ts b/apps/meteor/app/integrations/lib/outgoingEvents.ts new file mode 100644 index 000000000000..0beb18d0f092 --- /dev/null +++ b/apps/meteor/app/integrations/lib/outgoingEvents.ts @@ -0,0 +1,70 @@ +import type { OutgoingIntegrationEvent } from '@rocket.chat/core-typings'; + +export const outgoingEvents: Record< + OutgoingIntegrationEvent, + { label: string; value: OutgoingIntegrationEvent; use: { channel: boolean; triggerWords: boolean; targetRoom: boolean } } +> = { + sendMessage: { + label: 'Integrations_Outgoing_Type_SendMessage', + value: 'sendMessage', + use: { + channel: true, + triggerWords: true, + targetRoom: false, + }, + }, + fileUploaded: { + label: 'Integrations_Outgoing_Type_FileUploaded', + value: 'fileUploaded', + use: { + channel: true, + triggerWords: false, + targetRoom: false, + }, + }, + roomArchived: { + label: 'Integrations_Outgoing_Type_RoomArchived', + value: 'roomArchived', + use: { + channel: false, + triggerWords: false, + targetRoom: false, + }, + }, + roomCreated: { + label: 'Integrations_Outgoing_Type_RoomCreated', + value: 'roomCreated', + use: { + channel: false, + triggerWords: false, + targetRoom: false, + }, + }, + roomJoined: { + label: 'Integrations_Outgoing_Type_RoomJoined', + value: 'roomJoined', + use: { + channel: true, + triggerWords: false, + targetRoom: false, + }, + }, + roomLeft: { + label: 'Integrations_Outgoing_Type_RoomLeft', + value: 'roomLeft', + use: { + channel: true, + triggerWords: false, + targetRoom: false, + }, + }, + userCreated: { + label: 'Integrations_Outgoing_Type_UserCreated', + value: 'userCreated', + use: { + channel: false, + triggerWords: false, + targetRoom: true, + }, + }, +} as const; diff --git a/apps/meteor/app/integrations/lib/rocketchat.js b/apps/meteor/app/integrations/lib/rocketchat.js deleted file mode 100644 index 076a008d4c68..000000000000 --- a/apps/meteor/app/integrations/lib/rocketchat.js +++ /dev/null @@ -1,67 +0,0 @@ -export const integrations = { - outgoingEvents: { - sendMessage: { - label: 'Integrations_Outgoing_Type_SendMessage', - value: 'sendMessage', - use: { - channel: true, - triggerWords: true, - targetRoom: false, - }, - }, - fileUploaded: { - label: 'Integrations_Outgoing_Type_FileUploaded', - value: 'fileUploaded', - use: { - channel: true, - triggerWords: false, - targetRoom: false, - }, - }, - roomArchived: { - label: 'Integrations_Outgoing_Type_RoomArchived', - value: 'roomArchived', - use: { - channel: false, - triggerWords: false, - targetRoom: false, - }, - }, - roomCreated: { - label: 'Integrations_Outgoing_Type_RoomCreated', - value: 'roomCreated', - use: { - channel: false, - triggerWords: false, - targetRoom: false, - }, - }, - roomJoined: { - label: 'Integrations_Outgoing_Type_RoomJoined', - value: 'roomJoined', - use: { - channel: true, - triggerWords: false, - targetRoom: false, - }, - }, - roomLeft: { - label: 'Integrations_Outgoing_Type_RoomLeft', - value: 'roomLeft', - use: { - channel: true, - triggerWords: false, - targetRoom: false, - }, - }, - userCreated: { - label: 'Integrations_Outgoing_Type_UserCreated', - value: 'userCreated', - use: { - channel: false, - triggerWords: false, - targetRoom: true, - }, - }, - }, -}; diff --git a/apps/meteor/app/integrations/server/index.js b/apps/meteor/app/integrations/server/index.js index 6b796dbcd083..bbae6e42745a 100644 --- a/apps/meteor/app/integrations/server/index.js +++ b/apps/meteor/app/integrations/server/index.js @@ -1,6 +1,5 @@ -import '../lib/rocketchat'; import './logger'; -import './lib/validation'; +import './lib/validateOutgoingIntegration'; import './methods/incoming/addIncomingIntegration'; import './methods/incoming/updateIncomingIntegration'; import './methods/incoming/deleteIncomingIntegration'; diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index 17b9cbdf5c26..1576734295dd 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -14,7 +14,7 @@ import { Integrations, IntegrationHistory } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { getRoomByNameOrIdWithOptionToJoin, processWebhookMessage } from '../../../lib/server'; import { outgoingLogger } from '../logger'; -import { integrations } from '../../lib/rocketchat'; +import { outgoingEvents } from '../../lib/outgoingEvents'; import { fetch } from '../../../../server/lib/http/fetch'; export class RocketChatIntegrationHandler { @@ -30,7 +30,7 @@ export class RocketChatIntegrationHandler { addIntegration(record) { outgoingLogger.debug(`Adding the integration ${record.name} of the event ${record.event}!`); let channels; - if (record.event && !integrations.outgoingEvents[record.event].use.channel) { + if (record.event && !outgoingEvents[record.event].use.channel) { outgoingLogger.debug('The integration doesnt rely on channels.'); // We don't use any channels, so it's special ;) channels = ['__any']; @@ -667,7 +667,7 @@ export class RocketChatIntegrationHandler { let word; // Not all triggers/events support triggerWords - if (integrations.outgoingEvents[event].use.triggerWords) { + if (outgoingEvents[event].use.triggerWords) { if (trigger.triggerWords && trigger.triggerWords.length > 0) { for (const triggerWord of trigger.triggerWords) { if (!trigger.triggerWordAnywhere && message.msg.indexOf(triggerWord) === 0) { @@ -963,4 +963,4 @@ export class RocketChatIntegrationHandler { } } const triggerHandler = new RocketChatIntegrationHandler(); -export { integrations, triggerHandler }; +export { triggerHandler }; diff --git a/apps/meteor/app/integrations/server/lib/validation.js b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts similarity index 68% rename from apps/meteor/app/integrations/server/lib/validation.js rename to apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts index 9361680995e4..44bc2594baf7 100644 --- a/apps/meteor/app/integrations/server/lib/validation.js +++ b/apps/meteor/app/integrations/server/lib/validateOutgoingIntegration.ts @@ -2,21 +2,22 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; -import s from 'underscore.string'; +import type { IUser, INewOutgoingIntegration, IOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings'; -import { Rooms, Users, Subscriptions } from '../../../models'; -import { hasPermission, hasAllPermission } from '../../../authorization'; -import { integrations } from '../../lib/rocketchat'; +import { Rooms, Users, Subscriptions } from '../../../models/server'; +import { hasPermission, hasAllPermission } from '../../../authorization/server'; +import { outgoingEvents } from '../../lib/outgoingEvents'; +import { parseCSV } from '../../../../lib/utils/parseCSV'; const scopedChannels = ['all_public_channels', 'all_private_groups', 'all_direct_messages']; const validChannelChars = ['@', '#']; -function _verifyRequiredFields(integration) { +function _verifyRequiredFields(integration: INewOutgoingIntegration | IUpdateOutgoingIntegration): void { if ( !integration.event || !Match.test(integration.event, String) || integration.event.trim() === '' || - !integrations.outgoingEvents[integration.event] + !outgoingEvents[integration.event] ) { throw new Meteor.Error('error-invalid-event-type', 'Invalid event type', { function: 'validateOutgoing._verifyRequiredFields', @@ -29,7 +30,7 @@ function _verifyRequiredFields(integration) { }); } - if (integrations.outgoingEvents[integration.event].use.targetRoom && !integration.targetRoom) { + if (outgoingEvents[integration.event].use.targetRoom && !integration.targetRoom) { throw new Meteor.Error('error-invalid-targetRoom', 'Invalid Target Room', { function: 'validateOutgoing._verifyRequiredFields', }); @@ -41,13 +42,7 @@ function _verifyRequiredFields(integration) { }); } - for (const [index, url] of integration.urls.entries()) { - if (url.trim() === '') { - delete integration.urls[index]; - } - } - - integration.urls = _.without(integration.urls, [undefined]); + integration.urls = integration.urls.filter((url) => url && url.trim() !== ''); if (integration.urls.length === 0) { throw new Meteor.Error('error-invalid-urls', 'Invalid URLs', { @@ -56,7 +51,7 @@ function _verifyRequiredFields(integration) { } } -function _verifyUserHasPermissionForChannels(integration, userId, channels) { +function _verifyUserHasPermissionForChannels(userId: IUser['_id'], channels: string[]): void { for (let channel of channels) { if (scopedChannels.includes(channel)) { if (channel === 'all_public_channels') { @@ -102,18 +97,22 @@ function _verifyUserHasPermissionForChannels(integration, userId, channels) { } } -function _verifyRetryInformation(integration) { +function _verifyRetryInformation(integration: IOutgoingIntegration): void { if (!integration.retryFailedCalls) { return; } // Don't allow negative retry counts - integration.retryCount = integration.retryCount && parseInt(integration.retryCount) > 0 ? parseInt(integration.retryCount) : 4; + integration.retryCount = + integration.retryCount && parseInt(String(integration.retryCount)) > 0 ? parseInt(String(integration.retryCount)) : 4; integration.retryDelay = !integration.retryDelay || !integration.retryDelay.trim() ? 'powers-of-ten' : integration.retryDelay.toLowerCase(); } -integrations.validateOutgoing = function _validateOutgoing(integration, userId) { +export const validateOutgoingIntegration = function ( + integration: INewOutgoingIntegration | IUpdateOutgoingIntegration, + userId: IUser['_id'], +): IOutgoingIntegration { if (integration.channel && Match.test(integration.channel, String) && integration.channel.trim() === '') { delete integration.channel; } @@ -121,14 +120,14 @@ integrations.validateOutgoing = function _validateOutgoing(integration, userId) // Moved to it's own function to statisfy the complexity rule _verifyRequiredFields(integration); - let channels = []; - if (integrations.outgoingEvents[integration.event].use.channel) { + let channels: string[] = []; + if (outgoingEvents[integration.event].use.channel) { if (!Match.test(integration.channel, String)) { throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { function: 'validateOutgoing', }); } else { - channels = _.map(integration.channel.split(','), (channel) => s.trim(channel)); + channels = parseCSV(integration.channel); for (const channel of channels) { if (!validChannelChars.includes(channel[0]) && !scopedChannels.includes(channel.toLowerCase())) { @@ -144,22 +143,31 @@ integrations.validateOutgoing = function _validateOutgoing(integration, userId) }); } - if (integrations.outgoingEvents[integration.event].use.triggerWords && integration.triggerWords) { + const user = Users.findOne({ username: integration.username }); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user (did you delete the `rocket.cat` user?)', { function: 'validateOutgoing' }); + } + + const integrationData: IOutgoingIntegration = { + ...integration, + type: 'webhook-outgoing', + channel: channels, + userId: user._id, + _createdAt: new Date(), + _createdBy: Users.findOne(userId, { fields: { username: 1 } }), + }; + + if (outgoingEvents[integration.event].use.triggerWords && integration.triggerWords) { if (!Match.test(integration.triggerWords, [String])) { throw new Meteor.Error('error-invalid-triggerWords', 'Invalid triggerWords', { function: 'validateOutgoing', }); } - integration.triggerWords.forEach((word, index) => { - if (!word || word.trim() === '') { - delete integration.triggerWords[index]; - } - }); - - integration.triggerWords = _.without(integration.triggerWords, [undefined]); + integrationData.triggerWords = integration.triggerWords.filter((word) => word && word.trim() !== ''); } else { - delete integration.triggerWords; + delete integrationData.triggerWords; } if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { @@ -170,31 +178,21 @@ integrations.validateOutgoing = function _validateOutgoing(integration, userId) comments: false, }); - integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; - integration.scriptError = undefined; + integrationData.scriptCompiled = Babel.compile(integration.script, babelOptions).code; + integrationData.scriptError = undefined; } catch (e) { - integration.scriptCompiled = undefined; - integration.scriptError = _.pick(e, 'name', 'message', 'stack'); + integrationData.scriptCompiled = undefined; + integrationData.scriptError = _.pick(e, 'name', 'message', 'stack'); } } if (typeof integration.runOnEdits !== 'undefined') { // Verify this value is only true/false - integration.runOnEdits = integration.runOnEdits === true; - } - - _verifyUserHasPermissionForChannels(integration, userId, channels); - _verifyRetryInformation(integration); - - const user = Users.findOne({ username: integration.username }); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user (did you delete the `rocket.cat` user?)', { function: 'validateOutgoing' }); + integrationData.runOnEdits = integration.runOnEdits === true; } - integration.type = 'webhook-outgoing'; - integration.userId = user._id; - integration.channel = channels; + _verifyUserHasPermissionForChannels(userId, channels); + _verifyRetryInformation(integrationData); - return integration; + return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.js b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts similarity index 63% rename from apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.js rename to apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 478c4cb99314..43e063876a2e 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.js +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -1,8 +1,10 @@ import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; +import type { INewIncomingIntegration, IIncomingIntegration } from '@rocket.chat/core-typings'; import { hasPermission, hasAllPermission } from '../../../../authorization/server'; import { Users, Rooms, Subscriptions } from '../../../../models/server'; @@ -11,8 +13,26 @@ import { Integrations, Roles } from '../../../../models/server/raw'; const validChannelChars = ['@', '#']; Meteor.methods({ - async addIncomingIntegration(integration) { - if (!hasPermission(this.userId, 'manage-incoming-integrations') && !hasPermission(this.userId, 'manage-own-incoming-integrations')) { + async addIncomingIntegration(integration: INewIncomingIntegration): Promise { + const { userId } = this; + + check( + integration, + Match.ObjectIncluding({ + type: String, + name: String, + enabled: Boolean, + username: String, + channel: String, + alias: Match.Maybe(String), + emoji: Match.Maybe(String), + scriptEnabled: Boolean, + script: Match.Maybe(String), + avatar: Match.Maybe(String), + }), + ); + + if (!userId || (!hasPermission(userId, 'manage-incoming-integrations') && !hasPermission(userId, 'manage-own-incoming-integrations'))) { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'addIncomingIntegration', }); @@ -45,16 +65,35 @@ Meteor.methods({ method: 'addIncomingIntegration', }); } + + const user = Users.findOne({ username: integration.username }); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'addIncomingIntegration', + }); + } + + const integrationData: IIncomingIntegration = { + ...integration, + type: 'webhook-incoming', + channel: channels, + token: Random.id(48), + userId: user._id, + _createdAt: new Date(), + _createdBy: Users.findOne(userId, { fields: { username: 1 } }), + }; + if (integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') { try { let babelOptions = Babel.getDefaultOptions({ runtime: false }); babelOptions = _.extend(babelOptions, { compact: true, minified: true, comments: false }); - integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; - integration.scriptError = undefined; + integrationData.scriptCompiled = Babel.compile(integration.script, babelOptions).code; + integrationData.scriptError = undefined; } catch (e) { - integration.scriptCompiled = undefined; - integration.scriptError = _.pick(e, 'name', 'message', 'stack'); + integrationData.scriptCompiled = undefined; + integrationData.scriptError = _.pick(e, 'name', 'message', 'stack'); } } @@ -83,8 +122,8 @@ Meteor.methods({ } if ( - !hasAllPermission(this.userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations']) && - !Subscriptions.findOneByRoomIdAndUserId(record._id, this.userId, { fields: { _id: 1 } }) + !hasAllPermission(userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations']) && + !Subscriptions.findOneByRoomIdAndUserId(record._id, userId, { fields: { _id: 1 } }) ) { throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', { method: 'addIncomingIntegration', @@ -92,29 +131,12 @@ Meteor.methods({ } } - const user = Users.findOne({ username: integration.username }); - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'addIncomingIntegration', - }); - } - - const token = Random.id(48); - - integration.type = 'webhook-incoming'; - integration.token = token; - integration.channel = channels; - integration.userId = user._id; - integration._createdAt = new Date(); - integration._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - await Roles.addUserRoles(user._id, ['bot']); - const result = await Integrations.insertOne(integration); + const result = await Integrations.insertOne(integrationData); - integration._id = result.insertedId; + integrationData._id = result.insertedId; - return integration; + return integrationData; }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.js b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.js deleted file mode 100644 index 31eabe715c6c..000000000000 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../../authorization/server'; -import { Users } from '../../../../models/server'; -import { Integrations } from '../../../../models/server/raw'; -import { integrations } from '../../../lib/rocketchat'; - -Meteor.methods({ - async addOutgoingIntegration(integration) { - if (!hasPermission(this.userId, 'manage-outgoing-integrations') && !hasPermission(this.userId, 'manage-own-outgoing-integrations')) { - throw new Meteor.Error('not_authorized'); - } - - integration = integrations.validateOutgoing(integration, this.userId); - - integration._createdAt = new Date(); - integration._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - - const result = await Integrations.insertOne(integration); - integration._id = result.insertedId; - - return integration; - }, -}); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts new file mode 100644 index 000000000000..8514793d9213 --- /dev/null +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -0,0 +1,51 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; +import type { INewOutgoingIntegration, IOutgoingIntegration } from '@rocket.chat/core-typings'; + +import { hasPermission } from '../../../../authorization/server'; +import { Integrations } from '../../../../models/server/raw'; +import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; + +Meteor.methods({ + async addOutgoingIntegration(integration: INewOutgoingIntegration): Promise { + const { userId } = this; + + if (!userId || (!hasPermission(userId, 'manage-outgoing-integrations') && !hasPermission(userId, 'manage-own-outgoing-integrations'))) { + throw new Meteor.Error('not_authorized'); + } + + check( + integration, + Match.ObjectIncluding({ + type: String, + name: String, + enabled: Boolean, + username: String, + channel: String, + alias: Match.Maybe(String), + emoji: Match.Maybe(String), + scriptEnabled: Boolean, + script: Match.Maybe(String), + urls: Match.Maybe([String]), + event: Match.Maybe(String), + triggerWords: Match.Maybe([String]), + avatar: Match.Maybe(String), + token: Match.Maybe(String), + impersonateUser: Match.Maybe(Boolean), + retryCount: Match.Maybe(Number), + retryDelay: Match.Maybe(String), + retryFailedCalls: Match.Maybe(Boolean), + runOnEdits: Match.Maybe(Boolean), + targetRoom: Match.Maybe(String), + triggerWordAnywhere: Match.Maybe(Boolean), + }), + ); + + const integrationData = validateOutgoingIntegration(integration, userId); + + const result = await Integrations.insertOne(integrationData); + integrationData._id = result.insertedId; + + return integrationData; + }, +}); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js index ba48cb1b14cf..b357063cac9a 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js @@ -3,11 +3,11 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../../authorization/server'; import { Users } from '../../../../models/server'; import { Integrations } from '../../../../models/server/raw'; -import { integrations } from '../../../lib/rocketchat'; +import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; Meteor.methods({ async updateOutgoingIntegration(integrationId, integration) { - integration = integrations.validateOutgoing(integration, this.userId); + integration = validateOutgoingIntegration(integration, this.userId); if (!integration.token || integration.token.trim() === '') { throw new Meteor.Error('error-invalid-token', 'Invalid token', { diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 787686ff72c3..40d96fcb4c06 100644 --- a/apps/meteor/app/lib/server/functions/saveUser.js +++ b/apps/meteor/app/lib/server/functions/saveUser.js @@ -14,6 +14,7 @@ import { saveUserIdentity } from './saveUserIdentity'; import { checkEmailAvailability, checkUsernameAvailability, setUserAvatar, setEmail, setStatusText } from '.'; import { Users } from '../../../models/server'; import { callbacks } from '../../../../lib/callbacks'; +import { AppEvents, Apps } from '../../../apps/server/orchestrator'; const MAX_BIO_LENGTH = 260; const MAX_NICKNAME_LENGTH = 120; @@ -346,6 +347,8 @@ export const saveUser = function (userId, userData) { validateUserEditing(userId, userData); + const oldUserData = Users.findOneById(userId); + // update user if (userData.hasOwnProperty('username') || userData.hasOwnProperty('name')) { if ( @@ -411,6 +414,16 @@ export const saveUser = function (userId, userData) { callbacks.run('afterSaveUser', userData); + // App IPostUserUpdated event hook + const userUpdated = Users.findOneById(userId); + Promise.await( + Apps.triggerEvent(AppEvents.IPostUserUpdated, { + user: userUpdated, + previousUser: oldUserData, + performedBy: Meteor.user(), + }), + ); + if (sendPassword) { _sendUserEmail(settings.get('Password_Changed_Email_Subject'), passwordChangedHtml, userData); } diff --git a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts index cfdf804dbc39..6640f3cca105 100644 --- a/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts +++ b/apps/meteor/app/lib/server/methods/deleteUserOwnAccount.ts @@ -7,6 +7,7 @@ import s from 'underscore.string'; import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; import { deleteUser } from '../functions'; +import { AppEvents, Apps } from '../../../apps/server/orchestrator'; Meteor.methods({ async deleteUserOwnAccount(password, confirmRelinquish) { @@ -51,6 +52,9 @@ Meteor.methods({ await deleteUser(uid, confirmRelinquish); + // App IPostUserDeleted event hook + Promise.await(Apps.triggerEvent(AppEvents.IPostUserDeleted, { user })); + return true; }, }); diff --git a/apps/meteor/app/markdown/lib/parser/original/token.ts b/apps/meteor/app/markdown/lib/parser/original/token.ts index eccc700488a0..f251750ce0ed 100644 --- a/apps/meteor/app/markdown/lib/parser/original/token.ts +++ b/apps/meteor/app/markdown/lib/parser/original/token.ts @@ -3,22 +3,9 @@ * @param {String} msg - The message html */ import { Random } from 'meteor/random'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessage, TokenType, TokenExtra } from '@rocket.chat/core-typings'; -type TokenType = 'code' | 'inlinecode' | 'bold' | 'italic' | 'strike' | 'link'; -type Token = { - token: string; - type: TokenType; - text: string; - noHtml?: string; -} & TokenExtra; - -type TokenExtra = { - highlight?: boolean; - noHtml?: string; -}; - -export const addAsToken = (message: IMessage & { tokens: Token[] }, html: string, type: TokenType, extra?: TokenExtra): string => { +export const addAsToken = (message: IMessage, html: string, type: TokenType, extra?: TokenExtra): string => { if (!message.tokens) { message.tokens = []; } @@ -35,14 +22,14 @@ export const addAsToken = (message: IMessage & { tokens: Token[] }, html: string export const isToken = (msg: string): boolean => /=!=[.a-z0-9]{17}=!=/gim.test(msg.trim()); -export const validateAllowedTokens = (message: IMessage & { tokens: Token[] }, id: string, desiredTokens: TokenType[]): boolean => { +export const validateAllowedTokens = (message: IMessage, id: string, desiredTokens: TokenType[]): boolean => { const tokens = id.match(/=!=[.a-z0-9]{17}=!=/gim) || []; - const tokensFound = message.tokens.filter(({ token }) => tokens.includes(token)); - return tokensFound.length === 0 || tokensFound.every((token) => desiredTokens.includes(token.type)); + const tokensFound = message.tokens?.filter(({ token }) => tokens.includes(token)) || []; + return tokensFound.length === 0 || tokensFound.every((token) => token.type && desiredTokens.includes(token.type)); }; -export const validateForbiddenTokens = (message: IMessage & { tokens: Token[] }, id: string, desiredTokens: TokenType[]): boolean => { +export const validateForbiddenTokens = (message: IMessage, id: string, desiredTokens: TokenType[]): boolean => { const tokens = id.match(/=!=[.a-z0-9]{17}=!=/gim) || []; - const tokensFound = message.tokens.filter(({ token }) => tokens.includes(token)); - return tokensFound.length === 0 || !tokensFound.some((token) => desiredTokens.includes(token.type)); + const tokensFound = message.tokens?.filter(({ token }) => tokens.includes(token)) || []; + return tokensFound.length === 0 || !tokensFound.some((token) => token.type && desiredTokens.includes(token.type)); }; diff --git a/apps/meteor/app/models/server/raw/Integrations.ts b/apps/meteor/app/models/server/raw/Integrations.ts index f0f0a2e7f280..844d046ed28f 100644 --- a/apps/meteor/app/models/server/raw/Integrations.ts +++ b/apps/meteor/app/models/server/raw/Integrations.ts @@ -1,4 +1,4 @@ -import type { IIntegration } from '@rocket.chat/core-typings'; +import type { IIntegration, IUser } from '@rocket.chat/core-typings'; import { BaseRaw, IndexSpecification } from './BaseRaw'; @@ -32,7 +32,7 @@ export class IntegrationsRaw extends BaseRaw { createdBy, }: { _id: IIntegration['_id']; - createdBy: IIntegration['_createdBy']; + createdBy?: IUser['_id']; }): Promise { return this.findOne({ _id, diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx index 0b684b4d1488..685b9f29fbf4 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx @@ -94,7 +94,7 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug value={parentRoom} onChange={handleParentRoom} placeholder={t('Discussion_target_channel_description')} - disabled={defaultParentRoom} + disabled={Boolean(defaultParentRoom)} /> )} diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx index 616895ddc771..853dd7c7d293 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx @@ -109,9 +109,8 @@ const ForwardChatModal = ({ { + onChange={(value: any): void => { setValue('username', value); }} value={getValues().username} diff --git a/apps/meteor/client/components/RoomAutoComplete/Avatar.js b/apps/meteor/client/components/RoomAutoComplete/Avatar.js deleted file mode 100644 index 859cafdc8bd9..000000000000 --- a/apps/meteor/client/components/RoomAutoComplete/Avatar.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Options } from '@rocket.chat/fuselage'; -import React from 'react'; - -import RoomAvatar from '../avatar/RoomAvatar'; - -const Avatar = ({ value, type, avatarETag, ...props }) => ( - -); - -export default Avatar; diff --git a/apps/meteor/client/components/RoomAutoComplete/Avatar.tsx b/apps/meteor/client/components/RoomAutoComplete/Avatar.tsx new file mode 100644 index 000000000000..d90fa71772b4 --- /dev/null +++ b/apps/meteor/client/components/RoomAutoComplete/Avatar.tsx @@ -0,0 +1,16 @@ +import { Options } from '@rocket.chat/fuselage'; +import React, { ReactElement } from 'react'; + +import RoomAvatar from '../avatar/RoomAvatar'; + +type AvatarProps = { + value: string; + type: string; + avatarETag?: string | undefined; +}; + +const Avatar = ({ value, type, avatarETag, ...props }: AvatarProps): ReactElement => ( + +); + +export default Avatar; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.js b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx similarity index 56% rename from apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.js rename to apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index 79c6ade0ff0a..762bd2cc7ed1 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.js +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -1,13 +1,22 @@ import { AutoComplete, Option, Box } from '@rocket.chat/fuselage'; -import React, { memo, useMemo, useState } from 'react'; +import React, { ComponentProps, memo, ReactElement, useMemo, useState } from 'react'; import { useEndpointData } from '../../hooks/useEndpointData'; import RoomAvatar from '../avatar/RoomAvatar'; import Avatar from './Avatar'; -const query = (term = '') => ({ selector: JSON.stringify({ name: term }) }); +const query = ( + term = '', +): { + selector: string; +} => ({ selector: JSON.stringify({ name: term }) }); -const RoomAutoComplete = (props) => { +type RoomAutoCompleteProps = Omit, 'value' | 'filter'> & { + value: any; +}; + +/* @deprecated */ +const RoomAutoComplete = (props: RoomAutoCompleteProps): ReactElement => { const [filter, setFilter] = useState(''); const { value: data } = useEndpointData( 'rooms.autocomplete.channelAndPrivate', @@ -15,21 +24,19 @@ const RoomAutoComplete = (props) => { ); const options = useMemo( () => - (data && - data.items.map(({ name, _id, avatarETag, t }) => ({ - value: _id, - label: { name, avatarETag, type: t }, - }))) || - [], + data?.items.map(({ name, _id, avatarETag, t }) => ({ + value: _id, + label: { name, avatarETag, type: t }, + })) || [], [data], - ); + ) as unknown as { value: string; label: string }[]; return ( ( + renderSelected={({ value, label }): ReactElement => ( <> {' '} @@ -39,7 +46,7 @@ const RoomAutoComplete = (props) => { )} - renderItem={({ value, label, ...props }) => ( + renderItem={({ value, label, ...props }): ReactElement => (