diff --git a/.vscode/settings.json b/.vscode/settings.json index 2778300e470e..3d13987bf7ec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "changeProcessCWD": true } ], - "typescript.tsdk": "./node_modules/typescript/lib" + "typescript.tsdk": "./node_modules/typescript/lib", + "cSpell.words": ["photoswipe"] } diff --git a/_templates/package/new/package.json.ejs.t b/_templates/package/new/package.json.ejs.t index 51f678564c51..827f44a01ad6 100644 --- a/_templates/package/new/package.json.ejs.t +++ b/_templates/package/new/package.json.ejs.t @@ -11,7 +11,7 @@ to: packages/<%= name %>/package.json "eslint": "^8.12.0", "jest": "^27.5.1", "ts-jest": "^27.1.4", - "typescript": "~4.3.5" + "typescript": "~4.3.4" }, "scripts": { "lint": "eslint --ext .js,.jsx,.ts,.tsx .", diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index a19b1d6cfec3..daa663bcce20 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -38,7 +38,6 @@ spacebars standard-minifier-js@2.8.0 tracker@1.2.0 -#rocketchat:google-natural-language rocketchat:livechat rocketchat:streamer rocketchat:version 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/commands.ts b/apps/meteor/app/apps/server/bridges/commands.ts index f771f4014f5b..8a6726e3925a 100644 --- a/apps/meteor/app/apps/server/bridges/commands.ts +++ b/apps/meteor/app/apps/server/bridges/commands.ts @@ -8,7 +8,7 @@ import { Utilities } from '../../lib/misc/Utilities'; import { AppServerOrchestrator } from '../orchestrator'; export class AppCommandsBridge extends CommandBridge { - disabledCommands: Map; + disabledCommands: Map; // eslint-disable-next-line no-empty-function constructor(private readonly orch: AppServerOrchestrator) { @@ -40,7 +40,7 @@ export class AppCommandsBridge extends CommandBridge { throw new Error(`The command is not currently disabled: "${cmd}"`); } - slashCommands.commands[cmd] = this.disabledCommands.get(cmd); + slashCommands.commands[cmd] = this.disabledCommands.get(cmd) as typeof slashCommands.commands[string]; this.disabledCommands.delete(cmd); this.orch.getNotifier().commandUpdated(cmd); @@ -59,11 +59,13 @@ export class AppCommandsBridge extends CommandBridge { return; } - if (typeof slashCommands.commands[cmd] === 'undefined') { + const commandObj = slashCommands.commands[cmd]; + + if (typeof commandObj === 'undefined') { throw new Error(`Command does not exist in the system currently: "${cmd}"`); } - this.disabledCommands.set(cmd, slashCommands.commands[cmd]); + this.disabledCommands.set(cmd, commandObj); delete slashCommands.commands[cmd]; this.orch.getNotifier().commandDisabled(cmd); @@ -87,7 +89,9 @@ export class AppCommandsBridge extends CommandBridge { item.callback = this._appCommandExecutor.bind(this); item.providesPreview = command.providesPreview; item.previewer = command.previewer ? this._appCommandPreviewer.bind(this) : item.previewer; - item.previewCallback = command.executePreviewItem ? this._appCommandPreviewExecutor.bind(this) : item.previewCallback; + item.previewCallback = ( + command.executePreviewItem ? this._appCommandPreviewExecutor.bind(this) : item.previewCallback + ) as typeof slashCommands.commands[string]['previewCallback']; slashCommands.commands[cmd] = item; this.orch.getNotifier().commandUpdated(cmd); @@ -107,7 +111,9 @@ export class AppCommandsBridge extends CommandBridge { callback: this._appCommandExecutor.bind(this), providesPreview: command.providesPreview, previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), - previewCallback: !command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this), + previewCallback: (!command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this)) as + | typeof slashCommands.commands[string]['previewCallback'] + | undefined, }; slashCommands.commands[command.command.toLowerCase()] = item; diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index 3528aeebe588..57e0a9b63bba 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -19,6 +19,11 @@ export class AppListenerBridge { case AppInterface.IPreMessageUpdatedExtend: case AppInterface.IPreMessageUpdatedModify: case AppInterface.IPostMessageUpdated: + case AppInterface.IPostMessageReacted: + case AppInterface.IPostMessageFollowed: + case AppInterface.IPostMessagePinned: + case AppInterface.IPostMessageStarred: + case AppInterface.IPostMessageReported: return 'messageEvent'; case AppInterface.IPreRoomCreatePrevent: case AppInterface.IPreRoomCreateExtend: @@ -43,6 +48,13 @@ export class AppListenerBridge { case AppInterface.IPostLivechatGuestSaved: case AppInterface.IPostLivechatRoomSaved: return 'livechatEvent'; + case AppInterface.IPostUserCreated: + case AppInterface.IPostUserUpdated: + case AppInterface.IPostUserDeleted: + case AppInterface.IPostUserLogin: + case AppInterface.IPostUserLogout: + case AppInterface.IPostUserStatusChanged: + return 'userEvent'; default: return 'defaultEvent'; } @@ -55,9 +67,59 @@ export class AppListenerBridge { return this.orch.getManager().getListenerManager().executeListener(inte, payload); } - async messageEvent(inte, message) { + async messageEvent(inte, message, ...payload) { const msg = this.orch.getConverters().get('messages').convertMessage(message); - const result = await this.orch.getManager().getListenerManager().executeListener(inte, msg); + + const params = (() => { + switch (inte) { + case AppInterface.IPostMessageDeleted: + const [userDeleted] = payload; + return { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userDeleted), + }; + case AppInterface.IPostMessageReacted: + const [userReacted, reaction, isRemoved] = payload; + return { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userReacted), + reaction, + isRemoved, + }; + case AppInterface.IPostMessageFollowed: + const [userFollowed, isUnfollow] = payload; + return { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userFollowed), + isUnfollow, + }; + case AppInterface.IPostMessagePinned: + const [userPinned, isUnpinned] = payload; + return { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userPinned), + isUnpinned, + }; + case AppInterface.IPostMessageStarred: + const [userStarred, isStarred] = payload; + return { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userStarred), + isStarred, + }; + case AppInterface.IPostMessageReported: + const [userReported, reason] = payload; + return { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userReported), + reason, + }; + default: + return msg; + } + })(); + + const result = await this.orch.getManager().getListenerManager().executeListener(inte, params); if (typeof result === 'boolean') { return result; @@ -134,4 +196,34 @@ export class AppListenerBridge { return this.orch.getManager().getListenerManager().executeListener(inte, room); } } + + async userEvent(inte, data) { + let context; + switch (inte) { + case AppInterface.IPostUserLoggedIn: + case AppInterface.IPostUserLogout: + context = this.orch.getConverters().get('users').convertToApp(data.user); + return this.orch.getManager().getListenerManager().executeListener(inte, context); + case AppInterface.IPostUserStatusChanged: + const { currentStatus, previousStatus } = data; + context = { + user: this.orch.getConverters().get('users').convertToApp(data.user), + currentStatus, + previousStatus, + }; + + return this.orch.getManager().getListenerManager().executeListener(inte, context); + case AppInterface.IPostUserCreated: + case AppInterface.IPostUserUpdated: + case AppInterface.IPostUserDeleted: + 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/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.js index cadd96fbde7b..e8891b9dd720 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.js @@ -43,6 +43,11 @@ export class AppUsersConverter { lastLoginAt: user.lastLogin, appId: user.appId, customFields: user.customFields, + settings: { + preferences: { + ...(user?.settings?.preferences?.language && { language: user.settings.preferences.language }), + }, + }, }; } diff --git a/apps/meteor/app/apps/server/index.js b/apps/meteor/app/apps/server/index.js index ad3096af3158..753a21c4fbab 100644 --- a/apps/meteor/app/apps/server/index.js +++ b/apps/meteor/app/apps/server/index.js @@ -1,3 +1,4 @@ import './cron'; +import './status.ts'; export { Apps, AppEvents } from './orchestrator'; diff --git a/apps/meteor/app/apps/server/status.ts b/apps/meteor/app/apps/server/status.ts new file mode 100644 index 000000000000..3dd020df92ef --- /dev/null +++ b/apps/meteor/app/apps/server/status.ts @@ -0,0 +1,16 @@ +import { UserPresenceMonitor } from 'meteor/konecty:user-presence'; + +import { AppEvents, Apps } from './orchestrator'; + +UserPresenceMonitor.onSetUserStatus((...args: any) => { + const [user, status] = args; + + // App IPostUserStatusChanged event hook + Promise.await( + Apps.triggerEvent(AppEvents.IPostUserStatusChanged, { + user, + currentStatus: status, + previousStatus: user.status, + }), + ); +}); diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 557c6454fee0..d5c8f2e97efd 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; }); @@ -353,6 +358,15 @@ Accounts.validateLoginAttempt(function (login) { return callbacks.run('afterValidateLogin', login); }); + /** + * Trigger the event only when the + * user does login in Rocket.chat + */ + if (login.type !== 'resume') { + // App IPostUserLoggedIn event hook + Promise.await(Apps.triggerEvent(AppEvents.IPostUserLoggedIn, login.user)); + } + return true; }); 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/federation-v2/server/bridge.ts b/apps/meteor/app/federation-v2/server/bridge.ts index 289f7e9033d4..895e472de021 100644 --- a/apps/meteor/app/federation-v2/server/bridge.ts +++ b/apps/meteor/app/federation-v2/server/bridge.ts @@ -39,6 +39,10 @@ class Bridge { this.isRunning = false; } + public async getRoomStateByRoomId(userId: string, roomId: string): Promise[]> { + return Array.from(((await this.getInstance().getIntent(userId).roomState(roomId)) as IMatrixEvent[]) || []); + } + public getInstance(): MatrixBridge { return this.bridgeInstance; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts index 2accfbffbc8b..9282c3778639 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts +++ b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom.ts @@ -7,4 +7,5 @@ export enum AddMemberToRoomMembership { export interface IMatrixEventContentAddMemberToRoom { displayname: string; membership: AddMemberToRoomMembership; + is_direct?: boolean; } diff --git a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts index b374135c9309..6180c0356200 100644 --- a/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts +++ b/apps/meteor/app/federation-v2/server/definitions/IMatrixEventContent/IMatrixEventContentCreateRoom.ts @@ -1,4 +1,5 @@ export interface IMatrixEventContentCreateRoom { creator: string; room_version: string; + was_programatically_created?: boolean; } diff --git a/apps/meteor/app/federation-v2/server/events/createRoom.ts b/apps/meteor/app/federation-v2/server/events/createRoom.ts index 746f1f95c999..5cca3032c455 100644 --- a/apps/meteor/app/federation-v2/server/events/createRoom.ts +++ b/apps/meteor/app/federation-v2/server/events/createRoom.ts @@ -1,44 +1,187 @@ +import { IRoom, RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { ICreatedRoom } from '@rocket.chat/core-typings'; +import { IUser } from '@rocket.chat/apps-engine/definition/users'; + import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; +import { Rooms } from '../../../models/server/raw'; import { createRoom } from '../../../lib/server'; import { IMatrixEvent } from '../definitions/IMatrixEvent'; import { MatrixEventType } from '../definitions/MatrixEventType'; import { checkBridgedRoomExists } from '../methods/checkBridgedRoomExists'; import { matrixClient } from '../matrix-client'; +import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; +import { matrixBridge } from '../bridge'; +import { setRoomJoinRules } from './setRoomJoinRules'; +import { setRoomName } from './setRoomName'; +import { handleRoomMembership } from './roomMembership'; -export const handleCreateRoom = async (event: IMatrixEvent): Promise => { - const { room_id: matrixRoomId, sender } = event; +const removeUselessCharacterFromMatrixRoomId = (matrixRoomId: string): string => { + const prefixedRoomIdOnly = matrixRoomId.split(':')[0]; + const prefix = '!'; - return new Promise((resolve) => { - setTimeout(async () => { - // Check if the room already exists and if so, ignore - const roomExists = await checkBridgedRoomExists(matrixRoomId); + return prefixedRoomIdOnly?.replace(prefix, ''); +}; - if (roomExists) { - return resolve(); - } +const generateRoomNameForLocalServer = (matrixRoomId: string, matrixRoomName?: string): string => { + return matrixRoomName || `Federation-${removeUselessCharacterFromMatrixRoomId(matrixRoomId)}`; +}; + +const createLocalRoomAsync = async (roomType: RoomType, roomName: string, creator: IUser, members: IUser[] = []): Promise => { + return new Promise((resolve) => resolve(createRoom(roomType, roomName, creator.username, members as any[]) as ICreatedRoom)); +}; + +const createBridgedRecordRoom = async (roomId: IRoom['id'], matrixRoomId: string): Promise => + new Promise((resolve) => resolve(MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId }))); + +const createLocalUserIfNecessary = async (matrixUserId: string): Promise => { + const { uid } = await matrixClient.user.createLocal(matrixUserId); - // Find the bridged user id - const bridgedUserId = await MatrixBridgedUser.getId(sender); - let user; + return uid; +}; + +const applyRoomStateIfNecessary = async (matrixRoomId: string, roomState?: IMatrixEvent[]): Promise => { + // TODO: this should be better + /* eslint-disable no-await-in-loop */ + for (const state of roomState || []) { + switch (state.type) { + case 'm.room.create': + continue; + case 'm.room.join_rules': { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/camelcase + await setRoomJoinRules({ room_id: matrixRoomId, ...state }); - // Create the user if necessary - if (!bridgedUserId) { - const { uid } = await matrixClient.user.createLocal(sender); + break; + } + case 'm.room.name': { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/camelcase + await setRoomName({ room_id: matrixRoomId, ...state }); + + break; + } + case 'm.room.member': { + // @ts-ignore + if (state.content.membership === 'join') { + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/no-use-before-define + await handleRoomMembership({ room_id: matrixRoomId, ...state }); + } - user = Users.findOneById(uid); - } else { - user = await Users.findOneById(bridgedUserId); + break; } + } + } + /* eslint-enable no-await-in-loop */ +}; + +const mapLocalAndExternal = async (roomId: string, matrixRoomId: string): Promise => { + await createBridgedRecordRoom(roomId, matrixRoomId); + await Rooms.setAsBridged(roomId); +}; + +const tryToGetDataFromExternalRoom = async ( + senderMatrixUserId: string, + matrixRoomId: string, + roomState: IMatrixEvent[] = [], +): Promise> => { + const finalRoomState = + roomState && roomState?.length > 0 ? roomState : await matrixBridge.getRoomStateByRoomId(senderMatrixUserId, matrixRoomId); + const externalRoomName = finalRoomState.find((stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_NAME) + ?.content?.name; + const externalRoomJoinRule = finalRoomState.find( + (stateEvent: Record) => stateEvent.type === MatrixEventType.SET_ROOM_JOIN_RULES, + )?.content?.join_rule; - // Create temp room name - const roomName = `Federation-${matrixRoomId.split(':')[0].replace('!', '')}`; + return { + externalRoomName, + externalRoomJoinRule, + }; +}; + +export const createLocalDirectMessageRoom = async (matrixRoomId: string, creator: IUser, affectedUser: IUser): Promise => { + const { _id: roomId } = await createLocalRoomAsync(RoomType.DIRECT_MESSAGE, generateRoomNameForLocalServer(matrixRoomId), creator, [ + creator, + affectedUser, + ]); + await mapLocalAndExternal(roomId, matrixRoomId); + + return roomId; +}; + +export const getLocalRoomType = (matrixJoinRule = '', matrixRoomIsDirect = false): RoomType => { + const mapping: Record = { + [SetRoomJoinRules.JOIN]: RoomType.CHANNEL, + [SetRoomJoinRules.INVITE]: RoomType.PRIVATE_GROUP, + }; + const roomType = mapping[matrixJoinRule] || RoomType.CHANNEL; + + return roomType === RoomType.PRIVATE_GROUP && matrixRoomIsDirect ? RoomType.DIRECT_MESSAGE : roomType; +}; + +export const createLocalChannelsRoom = async ( + matrixRoomId: string, + senderMatrixUserId: string, + creator: IUser, + roomState?: IMatrixEvent[], +): Promise => { + let roomName = ''; + let joinRule; + + try { + const { externalRoomName, externalRoomJoinRule } = await tryToGetDataFromExternalRoom(senderMatrixUserId, matrixRoomId, roomState); + roomName = externalRoomName; + joinRule = externalRoomJoinRule; + } catch (err) { + // no-op + } + const { rid: roomId } = await createLocalRoomAsync( + getLocalRoomType(joinRule), + generateRoomNameForLocalServer(matrixRoomId, roomName), + creator, + ); + await mapLocalAndExternal(roomId, matrixRoomId); + + return roomId; +}; + +export const processFirstAccessFromExternalServer = async ( + matrixRoomId: string, + senderMatrixUserId: string, + affectedMatrixUserId: string, + senderUser: IUser, + affectedUser: IUser, + isDirect = false, + roomState: IMatrixEvent[], +): Promise => { + let roomId; + if (isDirect) { + roomId = await createLocalDirectMessageRoom(matrixRoomId, senderUser, affectedUser); + } else { + roomId = await createLocalChannelsRoom(matrixRoomId, senderMatrixUserId, senderUser, roomState); + } + + await applyRoomStateIfNecessary(matrixRoomId, roomState); + await matrixBridge.getInstance().getIntent(affectedMatrixUserId).join(matrixRoomId); + + return roomId; +}; + +export const handleCreateRoom = async (event: IMatrixEvent): Promise => { + const { + room_id: matrixRoomId, + sender, + content: { was_programatically_created: wasProgramaticallyCreated = false }, + } = event; - // @ts-ignore TODO: typing of legacy functions - const { rid: roomId } = createRoom('c', roomName, user.username); + // Check if the room already exists and if so, ignore + const roomExists = await checkBridgedRoomExists(matrixRoomId); + if (roomExists || wasProgramaticallyCreated) { + return; + } - MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId }); + const bridgedUserId = await MatrixBridgedUser.getId(sender); + const creator = await Users.findOneById(bridgedUserId || (await createLocalUserIfNecessary(sender))); - resolve(); - }, 500); - }); + await createLocalChannelsRoom(matrixRoomId, sender, creator); }; diff --git a/apps/meteor/app/federation-v2/server/events/roomMembership.ts b/apps/meteor/app/federation-v2/server/events/roomMembership.ts index 66479a4265cd..d51233c1f14e 100644 --- a/apps/meteor/app/federation-v2/server/events/roomMembership.ts +++ b/apps/meteor/app/federation-v2/server/events/roomMembership.ts @@ -1,68 +1,17 @@ -import { MatrixBridgedUser, MatrixBridgedRoom, Users, Rooms } from '../../../models/server'; -import { addUserToRoom, createRoom, removeUserFromRoom } from '../../../lib/server'; +import { IUser } from '@rocket.chat/apps-engine/definition/users'; + +import { MatrixBridgedUser, MatrixBridgedRoom, Users } from '../../../models/server'; +import { addUserToRoom, removeUserFromRoom } from '../../../lib/server'; import { IMatrixEvent } from '../definitions/IMatrixEvent'; import { MatrixEventType } from '../definitions/MatrixEventType'; import { AddMemberToRoomMembership } from '../definitions/IMatrixEventContent/IMatrixEventContentAddMemberToRoom'; -import { setRoomJoinRules } from './setRoomJoinRules'; -import { setRoomName } from './setRoomName'; -import { addToQueue } from '../queue'; import { matrixClient } from '../matrix-client'; +import { processFirstAccessFromExternalServer } from './createRoom'; -const ensureRoom = async ( - matrixRoomId: string, - roomId: string, - username: string, - roomState?: IMatrixEvent[], -): Promise => { - const room = await Rooms.findOneById(roomId); - // If the room does not exist, create it - if (!room) { - // Create temp room name - const roomName = `Federation-${matrixRoomId.split(':')[0].replace('!', '')}`; - - // @ts-ignore TODO: typing of legacy functions - const { rid: createdRoomId } = createRoom('c', roomName, username); - - roomId = createdRoomId; - - MatrixBridgedRoom.insert({ rid: roomId, mri: matrixRoomId }); - - // TODO: this should be better - /* eslint-disable no-await-in-loop */ - for (const state of roomState || []) { - switch (state.type) { - case 'm.room.create': - continue; - case 'm.room.join_rules': { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase - await setRoomJoinRules({ room_id: roomId, ...state }); +const extractServerNameFromMatrixUserId = (matrixRoomId = ''): string => matrixRoomId.split(':')[1]; - break; - } - case 'm.room.name': { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase - await setRoomName({ room_id: roomId, ...state }); - - break; - } - case 'm.room.member': { - // @ts-ignore - if (state.content.membership === 'join') { - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/no-use-before-define - await handleRoomMembership({ room_id: roomId, ...state }); - } - - break; - } - } - } - /* eslint-enable no-await-in-loop */ - } - - return roomId; +const addUserToRoomAsync = async (roomId: string, affectedUser: IUser, senderUser?: IUser): Promise => { + new Promise((resolve) => resolve(addUserToRoom(roomId, affectedUser as any, senderUser as any))); }; export const handleRoomMembership = async (event: IMatrixEvent): Promise => { @@ -70,15 +19,17 @@ export const handleRoomMembership = async (event: IMatrixEvent[], + ); + } + + if (!roomId) { + return; + } + switch (membership) { case AddMemberToRoomMembership.JOIN: - roomId = await ensureRoom(matrixRoomId, roomId, senderUser.username, roomState); - - addUserToRoom(roomId, affectedUser); + await addUserToRoomAsync(roomId, affectedUser); break; case AddMemberToRoomMembership.INVITE: - // Re-run the state first - if (!roomId) { - for (const state of roomState || []) { - // eslint-disable-next-line @typescript-eslint/camelcase,no-await-in-loop - addToQueue({ ...state, room_id: matrixRoomId }); - } - - addToQueue(event); - - return; - } - - // If the room exists, then just add the user // TODO: this should be a local invite - addUserToRoom(roomId, affectedUser, senderUser); + await addUserToRoomAsync(roomId, affectedUser, senderUser); break; case AddMemberToRoomMembership.LEAVE: - removeUserFromRoom(roomId, affectedUser, { + await removeUserFromRoom(roomId, affectedUser, { byUser: senderUser, }); break; diff --git a/apps/meteor/app/federation-v2/server/events/sendMessage.ts b/apps/meteor/app/federation-v2/server/events/sendMessage.ts index d2d35ac992df..c70577d1e2af 100644 --- a/apps/meteor/app/federation-v2/server/events/sendMessage.ts +++ b/apps/meteor/app/federation-v2/server/events/sendMessage.ts @@ -1,22 +1,28 @@ -// @ts-ignore -import { MatrixBridgedRoom, MatrixBridgedUser, Messages, Users } from '../../../models'; +import { MatrixBridgedRoom, MatrixBridgedUser, Users } from '../../../models/server'; import { IMatrixEvent } from '../definitions/IMatrixEvent'; import { MatrixEventType } from '../definitions/MatrixEventType'; +import { sendMessage } from '../../../lib/server'; +import { Rooms } from '../../../models/server/raw'; + +export const sendMessageAsync = async (user: any, msg: any, room: any): Promise => + new Promise((resolve) => resolve(sendMessage(user, msg, room))); export const handleSendMessage = async (event: IMatrixEvent): Promise => { const { room_id: matrixRoomId, sender } = event; + // Find the bridged room id + const roomId = await MatrixBridgedRoom.getId(matrixRoomId); + if (!roomId) { + return; + } + // Find the bridged user id const userId = await MatrixBridgedUser.getId(sender); // Find the user const user = await Users.findOneById(userId); - // Find the bridged room id - const roomId = await MatrixBridgedRoom.getId(matrixRoomId); + const room = await Rooms.findOneById(roomId); - Messages.createWithTypeRoomIdMessageAndUser('m', roomId, event.content.body, { - _id: user._id, - username: user.username, - }); + await sendMessageAsync(user, { msg: event.content.body }, room); }; diff --git a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts b/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts index 534e0b3043e6..e95bf691bf43 100644 --- a/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts +++ b/apps/meteor/app/federation-v2/server/events/setRoomJoinRules.ts @@ -1,4 +1,7 @@ -import { MatrixBridgedRoom, Rooms, Subscriptions } from '../../../models/server'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { Rooms, Subscriptions } from '../../../models/server/raw'; +import { MatrixBridgedRoom } from '../../../models/server'; import { IMatrixEvent } from '../definitions/IMatrixEvent'; import { MatrixEventType } from '../definitions/MatrixEventType'; import { SetRoomJoinRules } from '../definitions/IMatrixEventContent/IMatrixEventContentSetRoomJoinRules'; @@ -11,19 +14,28 @@ export const setRoomJoinRules = async (event: IMatrixEvent => { +const parametersForDirectMessagesIfNecessary = (room: IRoom, invitedUserId: string): Record => { + return room.t === RoomType.DIRECT_MESSAGE + ? { + // eslint-disable-next-line @typescript-eslint/camelcase + is_direct: true, + invite: [invitedUserId], + } + : {}; +}; + +export const create = async (inviterUser: IUser, room: IRoom, invitedUserId: string): Promise => { // Check if this room already exists (created by another method) // and if so, ignore the callback const roomMatrixId = MatrixBridgedRoom.getMatrixId(room._id); @@ -17,30 +29,38 @@ export const create = async (user: IUser, room: IRoom): Promise => { diff --git a/apps/meteor/app/federation-v2/server/settings.ts b/apps/meteor/app/federation-v2/server/settings.ts index 8e14bc68ca76..3be8a336af90 100644 --- a/apps/meteor/app/federation-v2/server/settings.ts +++ b/apps/meteor/app/federation-v2/server/settings.ts @@ -1,5 +1,8 @@ +import yaml from 'js-yaml'; import { SHA256 } from 'meteor/sha'; +import { getRegistrationInfo } from './config'; +import { Settings } from '../../models/server/raw'; import { settings, settingsRegistry } from '../../settings/server'; settingsRegistry.addGroup('Federation', function () { @@ -60,5 +63,72 @@ settingsRegistry.addGroup('Federation', function () { i18nLabel: 'Federation_Matrix_bridge_localpart', i18nDescription: 'Federation_Matrix_bridge_localpart_desc', }); + + this.add('Federation_Matrix_registration_file', '', { + readonly: true, + type: 'code', + i18nLabel: 'Federation_Matrix_registration_file', + i18nDescription: 'Federation_Matrix_registration_file_desc', + }); }); }); + +let registrationFile = {}; + +const updateRegistrationFile = async function (): Promise { + const registrationInfo = getRegistrationInfo(); + + // eslint-disable-next-line @typescript-eslint/camelcase + const { id, hs_token, as_token, sender_localpart } = registrationInfo; + let { url } = registrationInfo; + + if (!url || !url.includes(':')) { + url = `${url}:3300`; + } + + /* eslint-disable @typescript-eslint/camelcase */ + registrationFile = { + id, + hs_token, + as_token, + url, + sender_localpart, + namespaces: { + users: [ + { + exclusive: false, + regex: '.*', + }, + ], + rooms: [ + { + exclusive: false, + regex: '.*', + }, + ], + aliases: [ + { + exclusive: false, + regex: '.*', + }, + ], + }, + }; + /* eslint-enable @typescript-eslint/camelcase */ + + // Update the registration file + await Settings.updateValueById('Federation_Matrix_registration_file', yaml.dump(registrationFile)); +}; + +settings.watchMultiple( + [ + 'Federation_Matrix_id', + 'Federation_Matrix_hs_token', + 'Federation_Matrix_as_token', + 'Federation_Matrix_homeserver_url', + 'Federation_Matrix_homeserver_domain', + 'Federation_Matrix_bridge_url', + 'Federation_Matrix_bridge_localpart', + ], + updateRegistrationFile, +); diff --git a/apps/meteor/app/importer-csv/server/importer.js b/apps/meteor/app/importer-csv/server/importer.js index ce92a08c235d..d4d3d014dbfa 100644 --- a/apps/meteor/app/importer-csv/server/importer.js +++ b/apps/meteor/app/importer-csv/server/importer.js @@ -1,7 +1,7 @@ import { Random } from 'meteor/random'; import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import { Users } from '../../models/server'; +import { Users, Settings as SettingsRaw } from '../../models/server'; export class CsvImporter extends Base { constructor(info, importRecord) { @@ -119,7 +119,8 @@ export class CsvImporter extends Base { }); } - super.updateRecord({ 'count.users': parsedUsers.length }); + SettingsRaw.incrementValueById('CSV_Importer_Count', usersCount); + super.updateRecord({ 'count.users': usersCount }); return increaseProgressCount(); } diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js b/apps/meteor/app/importer-hipchat-enterprise/server/importer.js index c2c05c34032f..0e1fed9db759 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/importer.js @@ -5,6 +5,7 @@ import fs from 'fs'; import { Meteor } from 'meteor/meteor'; import { Base, ProgressStep } from '../../importer/server'; +import { Settings as SettingsRaw } from '../../models/server'; export class HipChatEnterpriseImporter extends Base { constructor(info, importRecord) { @@ -52,6 +53,7 @@ export class HipChatEnterpriseImporter extends Base { this.converter.addUser(newUser); } + SettingsRaw.incrementValueById('Hipchat_Enterprise_Importer_Count', count); super.updateRecord({ 'count.users': count }); super.addCountToTotal(count); } diff --git a/apps/meteor/app/importer-slack-users/server/importer.js b/apps/meteor/app/importer-slack-users/server/importer.js index 08c2992c766c..5fcf3924499b 100644 --- a/apps/meteor/app/importer-slack-users/server/importer.js +++ b/apps/meteor/app/importer-slack-users/server/importer.js @@ -5,6 +5,7 @@ import { Random } from 'meteor/random'; import { RawImports, Base, ProgressStep, Selection, SelectionUser } from '../../importer/server'; import { RocketChatFile } from '../../file'; import { Users } from '../../models'; +import { Settings as SettingsRaw } from '../../models/server'; export class SlackUsersImporter extends Base { constructor(info, importRecord) { @@ -164,6 +165,7 @@ export class SlackUsersImporter extends Base { }); } + SettingsRaw.incrementValueById('Slack_Users_Importer_Count', this.users.users.length); super.updateProgress(ProgressStep.FINISHING); super.updateProgress(ProgressStep.DONE); } catch (e) { diff --git a/apps/meteor/app/importer-slack/server/importer.js b/apps/meteor/app/importer-slack/server/importer.js index e2270576fcc5..8a9caa1775fe 100644 --- a/apps/meteor/app/importer-slack/server/importer.js +++ b/apps/meteor/app/importer-slack/server/importer.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import { Messages, ImportData } from '../../models/server'; +import { Messages, ImportData, Settings as SettingsRaw } from '../../models/server'; import { settings } from '../../settings/server'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; @@ -155,6 +155,7 @@ export class SlackImporter extends Base { } this.converter.addUser(newUser); + SettingsRaw.incrementValueById('Slack_Importer_Count'); } } 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..576c63071413 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']; @@ -564,7 +564,7 @@ export class RocketChatIntegrationHandler { }); room.usernames - .filter((username) => username !== message.u.username && this.triggers[`@${username}`]) + .filter((username) => username !== message?.u?.username && this.triggers[`@${username}`]) .forEach((username) => { for (const trigger of Object.values(this.triggers[`@${username}`])) { triggersToExecute.add(trigger); @@ -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/addUserToDefaultChannels.ts b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts index c7a47067ce62..62906fa893c1 100644 --- a/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts +++ b/apps/meteor/app/lib/server/functions/addUserToDefaultChannels.ts @@ -3,7 +3,7 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions, Messages } from '../../../models/server'; import { callbacks } from '../../../../lib/callbacks'; -export const addUserToDefaultChannels = function (user: IUser, silenced: boolean): void { +export const addUserToDefaultChannels = function (user: IUser, silenced?: boolean): void { callbacks.run('beforeJoinDefaultChannels', user); const defaultRooms = Rooms.findByDefaultAndTypes(true, ['c', 'p'], { fields: { usernames: 0 }, diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index f7f3271228e8..dfb7f473a0ae 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -69,6 +69,6 @@ export const deleteMessage = async function (message: IMessage, user: IUser): Pr } if (bridges) { - bridges.getListenerBridge().messageEvent('IPostMessageDeleted', deletedMsg); + bridges.getListenerBridge().messageEvent('IPostMessageDeleted', deletedMsg, user); } }; diff --git a/apps/meteor/app/lib/server/functions/getStatusText.ts b/apps/meteor/app/lib/server/functions/getStatusText.ts index af154b24d3fe..ad5ffcd84b4f 100644 --- a/apps/meteor/app/lib/server/functions/getStatusText.ts +++ b/apps/meteor/app/lib/server/functions/getStatusText.ts @@ -1,8 +1,8 @@ import { Users } from '../../../models/server'; -export const getStatusText = function (userId: string): unknown { +export const getStatusText = function (userId: string): string | undefined { if (!userId) { - return undefined; + return; } const fields = { diff --git a/apps/meteor/app/lib/server/functions/saveUser.js b/apps/meteor/app/lib/server/functions/saveUser.js index 787686ff72c3..5b5b1853060d 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(userData._id); + // 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/functions/setRealName.ts b/apps/meteor/app/lib/server/functions/setRealName.ts index 21c1c9b7362e..493b211aab02 100644 --- a/apps/meteor/app/lib/server/functions/setRealName.ts +++ b/apps/meteor/app/lib/server/functions/setRealName.ts @@ -8,15 +8,19 @@ import { hasPermission } from '../../../authorization/server'; import { RateLimiter } from '../lib'; import { api } from '../../../../server/sdk/api'; -export const _setRealName = function (userId: string, name: string, fullUser: IUser): unknown { +export const _setRealName = function (userId: string, name: string, fullUser: IUser): IUser | undefined { name = s.trim(name); if (!userId || (settings.get('Accounts_RequireNameForSignUp') && !name)) { - return false; + return; } const user = fullUser || Users.findOneById(userId); + if (!user) { + return; + } + // User already has desired name, return if (user.name && s.trim(user.name) === name) { return user; diff --git a/apps/meteor/app/lib/server/functions/setRoomAvatar.ts b/apps/meteor/app/lib/server/functions/setRoomAvatar.ts index 2873443e1fd3..1cd2a2442b75 100644 --- a/apps/meteor/app/lib/server/functions/setRoomAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setRoomAvatar.ts @@ -7,7 +7,7 @@ import { Rooms, Messages } from '../../../models/server'; import { Avatars } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; -export const setRoomAvatar = async function (rid: string, dataURI: string, user: IUser): Promise { +export const setRoomAvatar = async function (rid: string, dataURI: string, user: IUser): Promise { const fileStore = FileUpload.getStore('Avatars'); const current = await Avatars.findOneByRoomId(rid); diff --git a/apps/meteor/app/lib/server/functions/setStatusText.ts b/apps/meteor/app/lib/server/functions/setStatusText.ts index ee8e533a81a4..23bda1463602 100644 --- a/apps/meteor/app/lib/server/functions/setStatusText.ts +++ b/apps/meteor/app/lib/server/functions/setStatusText.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; +import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '../../../models/server'; import { Users as UsersRaw } from '../../../models/server/raw'; @@ -34,7 +35,7 @@ export const _setStatusTextPromise = async function (userId: string, statusText: return true; }; -export const _setStatusText = function (userId: any, statusText: string): unknown { +export const _setStatusText = function (userId: any, statusText: string): IUser | boolean { statusText = s.trim(statusText); if (statusText.length > 120) { statusText = statusText.substr(0, 120); 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/lib/server/methods/sendInvitationEmail.js b/apps/meteor/app/lib/server/methods/sendInvitationEmail.js index f5927e8c5a0e..89cbdce9eb97 100644 --- a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js +++ b/apps/meteor/app/lib/server/methods/sendInvitationEmail.js @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import * as Mailer from '../../../mailer'; import { hasPermission } from '../../../authorization'; import { settings } from '../../../settings'; +import { Settings as SettingsRaw } from '../../../models/server'; let html = ''; Meteor.startup(() => { @@ -37,7 +38,7 @@ Meteor.methods({ return validEmails.filter((email) => { try { - return Mailer.send({ + const mailerResult = Mailer.send({ to: email, from: settings.get('From_Email'), subject, @@ -46,6 +47,9 @@ Meteor.methods({ email, }, }); + + SettingsRaw.incrementValueById('Invitation_Email_Count'); + return mailerResult; } catch ({ message }) { throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { method: 'sendInvitationEmail', diff --git a/apps/meteor/app/lib/server/startup/email.ts b/apps/meteor/app/lib/server/startup/email.ts index b5e1d2f938c0..470599a41d52 100644 --- a/apps/meteor/app/lib/server/startup/email.ts +++ b/apps/meteor/app/lib/server/startup/email.ts @@ -474,6 +474,11 @@ settingsRegistry.addGroup('Email', function () { ); }); + this.add('Invitation_Email_Count', 0, { + type: 'int', + hidden: true, + }); + this.section('Forgot_password_section', function () { this.add('Forgot_Password_Email_Subject', '{Forgot_Password_Email_Subject}', { type: 'string', diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index b81c4e52d237..112241a71f69 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -180,6 +180,26 @@ settingsRegistry.addGroup('Accounts', function () { type: 'string', hidden: true, }); + this.add('Manual_Entry_User_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('CSV_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('Hipchat_Enterprise_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('Slack_Importer_Count', 0, { + type: 'int', + hidden: true, + }); + this.add('Slack_Users_Importer_Count', 0, { + type: 'int', + hidden: true, + }); this.add('Accounts_UseDefaultBlockedDomainsList', true, { type: 'boolean', }); @@ -3222,6 +3242,15 @@ settingsRegistry.addGroup('Call_Center', function () { value: true, }, }); + this.add('VoIP_Enable_Keep_Alive_For_Unstable_Networks', true, { + type: 'boolean', + public: true, + i18nDescription: 'VoIP_Enable_Keep_Alive_For_Unstable_Networks_Description', + enableQuery: { + _id: 'Livechat_enabled', + value: true, + }, + }); }); this.section('Management_Server', function () { 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/message-pin/server/pinMessage.js b/apps/meteor/app/message-pin/server/pinMessage.js index 07aa7885c7cb..6f9c11d51589 100644 --- a/apps/meteor/app/message-pin/server/pinMessage.js +++ b/apps/meteor/app/message-pin/server/pinMessage.js @@ -7,6 +7,7 @@ import { isTheLastMessage } from '../../lib/server'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { canAccessRoom, hasPermission, roomAccessAttributes } from '../../authorization/server'; import { Subscriptions, Messages, Users, Rooms } from '../../models'; +import { Apps, AppEvents } from '../../apps/server/orchestrator'; const recursiveRemove = (msg, deep = 1) => { if (!msg) { @@ -101,6 +102,9 @@ Meteor.methods({ }); } + // App IPostMessagePinned event hook + Promise.await(Apps.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, Meteor.user(), originalMessage.pinned)); + return Messages.createWithTypeRoomIdMessageAndUser('message_pinned', originalMessage.rid, '', me, { attachments: [ { @@ -173,6 +177,9 @@ Meteor.methods({ Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); } + // App IPostMessagePinned event hook + Promise.await(Apps.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, Meteor.user(), originalMessage.pinned)); + return Messages.setPinnedByIdAndUserId(originalMessage._id, originalMessage.pinnedBy, originalMessage.pinned); }, }); diff --git a/apps/meteor/app/message-star/server/starMessage.js b/apps/meteor/app/message-star/server/starMessage.js index 66c908524a15..8e4292170750 100644 --- a/apps/meteor/app/message-star/server/starMessage.js +++ b/apps/meteor/app/message-star/server/starMessage.js @@ -4,6 +4,7 @@ import { settings } from '../../settings/server'; import { isTheLastMessage } from '../../lib/server'; import { canAccessRoom, roomAccessAttributes } from '../../authorization/server'; import { Subscriptions, Rooms, Messages } from '../../models/server'; +import { Apps, AppEvents } from '../../apps/server/orchestrator'; Meteor.methods({ starMessage(message) { @@ -40,6 +41,8 @@ Meteor.methods({ Rooms.updateLastMessageStar(room._id, Meteor.userId(), message.starred); } + Promise.await(Apps.triggerEvent(AppEvents.IPostMessageStarred, message, Meteor.user(), message.starred)); + return Messages.updateUserStarById(message._id, Meteor.userId(), message.starred); }, }); diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index bc8932afe570..593fdab626da 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -26,6 +26,7 @@ export class Rooms extends Base { // field used for DMs only this.tryEnsureIndex({ uids: 1 }, { sparse: true }); this.tryEnsureIndex({ createdOTR: 1 }, { sparse: true }); + this.tryEnsureIndex({ encrypted: 1 }, { sparse: true }); // used on statistics this.tryEnsureIndex( { diff --git a/apps/meteor/app/models/server/models/Settings.js b/apps/meteor/app/models/server/models/Settings.js index 19d7059e76a7..c2ef98d35476 100644 --- a/apps/meteor/app/models/server/models/Settings.js +++ b/apps/meteor/app/models/server/models/Settings.js @@ -145,7 +145,7 @@ export class Settings extends Base { return this.update(query, update); } - incrementValueById(_id) { + incrementValueById(_id, value = 1) { const query = { blocked: { $ne: true }, _id, @@ -153,7 +153,7 @@ export class Settings extends Base { const update = { $inc: { - value: 1, + value, }, }; diff --git a/apps/meteor/app/models/server/models/Subscriptions.js b/apps/meteor/app/models/server/models/Subscriptions.js index 5286c2bab955..2f68de208262 100644 --- a/apps/meteor/app/models/server/models/Subscriptions.js +++ b/apps/meteor/app/models/server/models/Subscriptions.js @@ -227,6 +227,27 @@ export class Subscriptions extends Base { return this.update(query, update); } + updateHideMentionStatusById(_id, hideMentionStatus) { + const query = { + _id, + }; + + const update = + hideMentionStatus === true + ? { + $set: { + hideMentionStatus, + }, + } + : { + $unset: { + hideMentionStatus: 1, + }, + }; + + return this.update(query, update); + } + updateMuteGroupMentions(_id, muteGroupMentions) { const query = { _id, diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index 0a794af7e32f..3e8eb705d3d3 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -60,6 +60,9 @@ export class Users extends Base { this.tryEnsureIndex({ extension: 1 }, { sparse: true, unique: true }); this.tryEnsureIndex({ language: 1 }, { sparse: true }); + this.tryEnsureIndex({ 'active': 1, 'services.email2fa.enabled': 1 }, { sparse: true }); // used by statistics + this.tryEnsureIndex({ 'active': 1, 'services.totp.enabled': 1 }, { sparse: true }); // used by statistics + const collectionObj = this.model.rawCollection(); this.findAndModify = Meteor.wrapAsync(collectionObj.findAndModify, collectionObj); } 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/app/models/server/raw/Invites.ts b/apps/meteor/app/models/server/raw/Invites.ts index 2f3e9d835a92..ef55e009ca8e 100644 --- a/apps/meteor/app/models/server/raw/Invites.ts +++ b/apps/meteor/app/models/server/raw/Invites.ts @@ -27,4 +27,12 @@ export class InvitesRaw extends BaseRaw { }, ); } + + async countUses(): Promise { + const [result] = await this.col + .aggregate<{ totalUses: number } | undefined>([{ $group: { _id: null, totalUses: { $sum: '$uses' } } }]) + .toArray(); + + return result?.totalUses || 0; + } } diff --git a/apps/meteor/app/models/server/raw/Messages.js b/apps/meteor/app/models/server/raw/Messages.js index ce0343ded065..7f84cac25420 100644 --- a/apps/meteor/app/models/server/raw/Messages.js +++ b/apps/meteor/app/models/server/raw/Messages.js @@ -188,4 +188,46 @@ export class MessagesRaw extends BaseRaw { options, ); } + + async countRoomsWithStarredMessages(options) { + const [queryResult] = await this.col + .aggregate( + [{ $match: { 'starred._id': { $exists: true } } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], + options, + ) + .toArray(); + + return queryResult?.total || 0; + } + + async countRoomsWithPinnedMessages(options) { + const [queryResult] = await this.col + .aggregate([{ $match: { pinned: true } }, { $group: { _id: '$rid' } }, { $group: { _id: null, total: { $sum: 1 } } }], options) + .toArray(); + + return queryResult?.total || 0; + } + + async countE2EEMessages(options) { + return this.find({ t: 'e2e' }, options).count(); + } + + findPinned(options) { + const query = { + t: { $ne: 'rm' }, + _hidden: { $ne: true }, + pinned: true, + }; + + return this.find(query, options); + } + + findStarred(options) { + const query = { + '_hidden': { $ne: true }, + 'starred._id': { $exists: true }, + }; + + return this.find(query, options); + } } diff --git a/apps/meteor/app/models/server/raw/Rooms.js b/apps/meteor/app/models/server/raw/Rooms.js index a7e338db5f68..0d86223b42a8 100644 --- a/apps/meteor/app/models/server/raw/Rooms.js +++ b/apps/meteor/app/models/server/raw/Rooms.js @@ -460,4 +460,25 @@ export class RoomsRaw extends BaseRaw { }, ]); } + + setAsBridged(roomId) { + return this.updateOne({ _id: roomId }, { $set: { bridged: true } }); + } + + findByE2E(options) { + return this.find( + { + encrypted: true, + }, + options, + ); + } + + findRoomsInsideTeams(autoJoin = false) { + return this.find({ + teamId: { $exists: true }, + teamMain: { $exists: false }, + ...(autoJoin && { teamDefault: true }), + }); + } } diff --git a/apps/meteor/app/models/server/raw/Users.js b/apps/meteor/app/models/server/raw/Users.js index 29dfd1c3a49f..c6b4f0809d22 100644 --- a/apps/meteor/app/models/server/raw/Users.js +++ b/apps/meteor/app/models/server/raw/Users.js @@ -999,4 +999,20 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } + + findActiveUsersTOTPEnable(options) { + const query = { + 'active': true, + 'services.totp.enabled': true, + }; + return this.find(query, options); + } + + findActiveUsersEmail2faEnable(options) { + const query = { + 'active': true, + 'services.email2fa.enabled': true, + }; + return this.find(query, options); + } } diff --git a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.js b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.js index 13311a498bc6..4d009c2a8db0 100644 --- a/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.js +++ b/apps/meteor/app/push-notifications/server/methods/saveNotificationSettings.js @@ -63,6 +63,9 @@ Meteor.methods({ hideUnreadStatus: { updateMethod: (subscription, value) => Subscriptions.updateHideUnreadStatusById(subscription._id, value === '1'), }, + hideMentionStatus: { + updateMethod: (subscription, value) => Subscriptions.updateHideMentionStatusById(subscription._id, value === '1'), + }, muteGroupMentions: { updateMethod: (subscription, value) => Subscriptions.updateMuteGroupMentions(subscription._id, value === '1'), }, diff --git a/apps/meteor/app/reactions/server/setReaction.js b/apps/meteor/app/reactions/server/setReaction.js index 0770d14bb118..336747f1af4a 100644 --- a/apps/meteor/app/reactions/server/setReaction.js +++ b/apps/meteor/app/reactions/server/setReaction.js @@ -9,6 +9,7 @@ import { emoji } from '../../emoji/server'; import { isTheLastMessage, msgStream } from '../../lib/server'; import { canAccessRoom, hasPermission } from '../../authorization/server'; import { api } from '../../../server/sdk/api'; +import { AppEvents, Apps } from '../../apps/server/orchestrator'; const removeUserReaction = (message, reaction, username) => { message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(username), 1); @@ -52,6 +53,9 @@ async function setReaction(room, user, message, reaction, shouldReact) { if (userAlreadyReacted === shouldReact) { return; } + + let isReacted; + if (userAlreadyReacted) { removeUserReaction(message, reaction, user.username); if (_.isEmpty(message.reactions)) { @@ -68,6 +72,8 @@ async function setReaction(room, user, message, reaction, shouldReact) { } callbacks.run('unsetReaction', message._id, reaction); callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact }); + + isReacted = false; } else { if (!message.reactions) { message.reactions = {}; @@ -84,8 +90,12 @@ async function setReaction(room, user, message, reaction, shouldReact) { } callbacks.run('setReaction', message._id, reaction); callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); + + isReacted = true; } + Promise.await(Apps.triggerEvent(AppEvents.IPostMessageReacted, message, Meteor.user(), reaction, isReacted)); + msgStream.emit(message.rid, message); } diff --git a/apps/meteor/app/retention-policy/index.js b/apps/meteor/app/retention-policy/index.js deleted file mode 100644 index ca39cd0df4b1..000000000000 --- a/apps/meteor/app/retention-policy/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './server/index'; diff --git a/apps/meteor/app/slashcommands-me/index.ts b/apps/meteor/app/retention-policy/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-me/index.ts rename to apps/meteor/app/retention-policy/index.ts diff --git a/apps/meteor/app/retention-policy/server/cronPruneMessages.js b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts similarity index 71% rename from apps/meteor/app/retention-policy/server/cronPruneMessages.js rename to apps/meteor/app/retention-policy/server/cronPruneMessages.ts index 2a26df45df56..1d4ce3169bff 100644 --- a/apps/meteor/app/retention-policy/server/cronPruneMessages.js +++ b/apps/meteor/app/retention-policy/server/cronPruneMessages.ts @@ -1,12 +1,13 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; +import { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { Rooms } from '../../models/server'; -import { cleanRoomHistory } from '../../lib'; +import { cleanRoomHistory } from '../../lib/server'; -let types = []; - -const oldest = new Date('0001-01-01T00:00:00Z'); +interface IParser { + cron(val: string): string; +} const maxTimes = { c: 0, @@ -14,14 +15,18 @@ const maxTimes = { d: 0, }; -const toDays = (d) => d * 1000 * 60 * 60 * 24; +let types: (keyof typeof maxTimes)[] = []; + +const oldest = new Date('0001-01-01T00:00:00Z'); + +const toDays = (d: number): number => d * 1000 * 60 * 60 * 24; -function job() { +function job(): void { const now = new Date(); - const filesOnly = settings.get('RetentionPolicy_FilesOnly'); - const excludePinned = settings.get('RetentionPolicy_DoNotPrunePinned'); - const ignoreDiscussion = settings.get('RetentionPolicy_DoNotPruneDiscussion'); - const ignoreThreads = settings.get('RetentionPolicy_DoNotPruneThreads'); + const filesOnly = settings.get('RetentionPolicy_FilesOnly'); + const excludePinned = settings.get('RetentionPolicy_DoNotPrunePinned'); + const ignoreDiscussion = settings.get('RetentionPolicy_DoNotPruneDiscussion'); + const ignoreThreads = settings.get('RetentionPolicy_DoNotPruneThreads'); // get all rooms with default values types.forEach((type) => { @@ -35,7 +40,7 @@ function job() { 'retention.overrideGlobal': { $ne: true }, }, { fields: { _id: 1 } }, - ).forEach(({ _id: rid }) => { + ).forEach(({ _id: rid }: IRoomWithRetentionPolicy) => { cleanRoomHistory({ rid, latest, @@ -52,7 +57,7 @@ function job() { 'retention.enabled': { $eq: true }, 'retention.overrideGlobal': { $eq: true }, 'retention.maxAge': { $gte: 0 }, - }).forEach((room) => { + }).forEach((room: IRoomWithRetentionPolicy) => { const { maxAge = 30, filesOnly, excludePinned, ignoreThreads } = room.retention; const latest = new Date(now.getTime() - toDays(maxAge)); cleanRoomHistory({ @@ -67,7 +72,7 @@ function job() { }); } -function getSchedule(precision) { +function getSchedule(precision: '0' | '1' | '2' | '3'): string { switch (precision) { case '0': return '*/30 * * * *'; // 30 minutes @@ -82,8 +87,8 @@ function getSchedule(precision) { const pruneCronName = 'Prune old messages by retention policy'; -function deployCron(precision) { - const schedule = (parser) => parser.cron(precision); +function deployCron(precision: string): void { + const schedule = (parser: IParser): string => parser.cron(precision); SyncedCron.remove(pruneCronName); SyncedCron.add({ @@ -129,7 +134,7 @@ settings.watchMultiple( maxTimes.d = settings.get('RetentionPolicy_MaxAge_DMs'); const precision = - (settings.get('RetentionPolicy_Advanced_Precision') && settings.get('RetentionPolicy_Advanced_Precision_Cron')) || + (settings.get('RetentionPolicy_Advanced_Precision') && settings.get('RetentionPolicy_Advanced_Precision_Cron')) || getSchedule(settings.get('RetentionPolicy_Precision')); return deployCron(precision); diff --git a/apps/meteor/app/retention-policy/server/index.js b/apps/meteor/app/retention-policy/server/index.ts similarity index 100% rename from apps/meteor/app/retention-policy/server/index.js rename to apps/meteor/app/retention-policy/server/index.ts diff --git a/apps/meteor/app/slackbridge/server/SlackAdapter.js b/apps/meteor/app/slackbridge/server/SlackAdapter.js index d17d5b64cbed..4e597f43f1ef 100644 --- a/apps/meteor/app/slackbridge/server/SlackAdapter.js +++ b/apps/meteor/app/slackbridge/server/SlackAdapter.js @@ -2,7 +2,7 @@ import url from 'url'; import http from 'http'; import https from 'https'; -import { RTMClient } from '@slack/client'; +import { RTMClient } from '@slack/rtm-api'; import { Meteor } from 'meteor/meteor'; import { slackLogger } from './logger'; diff --git a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js b/apps/meteor/app/slackbridge/server/slackbridge_import.server.js index 937eef85c121..e41cb15255cf 100644 --- a/apps/meteor/app/slackbridge/server/slackbridge_import.server.js +++ b/apps/meteor/app/slackbridge/server/slackbridge_import.server.js @@ -85,7 +85,6 @@ function SlackBridgeImport(command, params, item) { }); throw error; } - return SlackBridgeImport; } slashCommands.add('slackbridge-import', SlackBridgeImport); diff --git a/apps/meteor/app/slashcommand-asciiarts/client/index.js b/apps/meteor/app/slashcommand-asciiarts/client/index.ts similarity index 100% rename from apps/meteor/app/slashcommand-asciiarts/client/index.js rename to apps/meteor/app/slashcommand-asciiarts/client/index.ts diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/gimme.js b/apps/meteor/app/slashcommand-asciiarts/lib/gimme.js deleted file mode 100644 index 752597ad5c78..000000000000 --- a/apps/meteor/app/slashcommand-asciiarts/lib/gimme.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { slashCommands } from '../../utils'; -/* - * Gimme is a named function that will replace /gimme commands - * @param {Object} message - The message object - */ - -function Gimme(command, params, item) { - if (command === 'gimme') { - const msg = item; - msg.msg = `༼ つ ◕_◕ ༽つ ${params}`; - Meteor.call('sendMessage', msg); - } -} - -slashCommands.add('gimme', Gimme, { - description: 'Slash_Gimme_Description', - params: 'your_message_optional', -}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts b/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts new file mode 100644 index 000000000000..a057a5faa542 --- /dev/null +++ b/apps/meteor/app/slashcommand-asciiarts/lib/gimme.ts @@ -0,0 +1,19 @@ +import { Meteor } from 'meteor/meteor'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +/* + * Gimme is a named function that will replace /gimme commands + * @param {Object} message - The message object + */ + +function Gimme(_command: 'gimme', params: string, item: IMessage): void { + const msg = item; + msg.msg = `༼ つ ◕_◕ ༽つ ${params}`; + Meteor.call('sendMessage', msg); +} + +slashCommands.add('gimme', Gimme, { + description: 'Slash_Gimme_Description', + params: 'your_message_optional', +}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/lenny.js b/apps/meteor/app/slashcommand-asciiarts/lib/lenny.js deleted file mode 100644 index 95afdc242459..000000000000 --- a/apps/meteor/app/slashcommand-asciiarts/lib/lenny.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { slashCommands } from '../../utils'; -/* - * Lenny is a named function that will replace /lenny commands - * @param {Object} message - The message object - */ - -function LennyFace(command, params, item) { - if (command === 'lennyface') { - const msg = item; - msg.msg = `${params} ( ͡° ͜ʖ ͡°)`; - Meteor.call('sendMessage', msg); - } -} - -slashCommands.add('lennyface', LennyFace, { - description: 'Slash_LennyFace_Description', - params: 'your_message_optional', -}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts b/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts new file mode 100644 index 000000000000..135090952227 --- /dev/null +++ b/apps/meteor/app/slashcommand-asciiarts/lib/lenny.ts @@ -0,0 +1,19 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +/* + * Lenny is a named function that will replace /lenny commands + * @param {Object} message - The message object + */ + +function LennyFace(_command: 'lennyface', params: string, item: IMessage): void { + const msg = item; + msg.msg = `${params} ( ͡° ͜ʖ ͡°)`; + Meteor.call('sendMessage', msg); +} + +slashCommands.add('lennyface', LennyFace, { + description: 'Slash_LennyFace_Description', + params: 'your_message_optional', +}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/shrug.js b/apps/meteor/app/slashcommand-asciiarts/lib/shrug.js deleted file mode 100644 index 05d76fdb8db1..000000000000 --- a/apps/meteor/app/slashcommand-asciiarts/lib/shrug.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { slashCommands } from '../../utils'; -/* - * Shrug is a named function that will replace /shrug commands - * @param {Object} message - The message object - */ - -function Shrug(command, params, item) { - if (command === 'shrug') { - const msg = item; - msg.msg = `${params} ¯\\_(ツ)_/¯`; - Meteor.call('sendMessage', msg); - } -} - -slashCommands.add('shrug', Shrug, { - description: 'Slash_Shrug_Description', - params: 'your_message_optional', -}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts b/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts new file mode 100644 index 000000000000..b18e1fd1b039 --- /dev/null +++ b/apps/meteor/app/slashcommand-asciiarts/lib/shrug.ts @@ -0,0 +1,19 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +/* + * Shrug is a named function that will replace /shrug commands + * @param {Object} message - The message object + */ + +function Shrug(_command: 'shrug', params: string, item: IMessage): void { + const msg = item; + msg.msg = `${params} ¯\\_(ツ)_/¯`; + Meteor.call('sendMessage', msg); +} + +slashCommands.add('shrug', Shrug, { + description: 'Slash_Shrug_Description', + params: 'your_message_optional', +}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.js b/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.js deleted file mode 100644 index 5454586d4ec3..000000000000 --- a/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { slashCommands } from '../../utils'; -/* - * Tableflip is a named function that will replace /Tableflip commands - * @param {Object} message - The message object - */ - -function Tableflip(command, params, item) { - if (command === 'tableflip') { - const msg = item; - msg.msg = `${params} (╯°□°)╯︵ ┻━┻`; - Meteor.call('sendMessage', msg); - } -} - -slashCommands.add('tableflip', Tableflip, { - description: 'Slash_Tableflip_Description', - params: 'your_message_optional', -}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts b/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts new file mode 100644 index 000000000000..c2663ec487b3 --- /dev/null +++ b/apps/meteor/app/slashcommand-asciiarts/lib/tableflip.ts @@ -0,0 +1,19 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +/* + * Tableflip is a named function that will replace /Tableflip commands + * @param {Object} message - The message object + */ + +function Tableflip(_command: 'tableflip', params: string, item: IMessage): void { + const msg = item; + msg.msg = `${params} (╯°□°)╯︵ ┻━┻`; + Meteor.call('sendMessage', msg); +} + +slashCommands.add('tableflip', Tableflip, { + description: 'Slash_Tableflip_Description', + params: 'your_message_optional', +}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/unflip.js b/apps/meteor/app/slashcommand-asciiarts/lib/unflip.js deleted file mode 100644 index d5728d60ad62..000000000000 --- a/apps/meteor/app/slashcommand-asciiarts/lib/unflip.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { slashCommands } from '../../utils'; -/* - * Unflip is a named function that will replace /unflip commands - * @param {Object} message - The message object - */ - -function Unflip(command, params, item) { - if (command === 'unflip') { - const msg = item; - msg.msg = `${params} ┬─┬ ノ( ゜-゜ノ)`; - Meteor.call('sendMessage', msg); - } -} - -slashCommands.add('unflip', Unflip, { - description: 'Slash_TableUnflip_Description', - params: 'your_message_optional', -}); diff --git a/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts b/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts new file mode 100644 index 000000000000..5f66b2c8c85c --- /dev/null +++ b/apps/meteor/app/slashcommand-asciiarts/lib/unflip.ts @@ -0,0 +1,19 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +/* + * Unflip is a named function that will replace /unflip commands + * @param {Object} message - The message object + */ + +function Unflip(_command: 'unflip', params: string, item: IMessage): void { + const msg = item; + msg.msg = `${params} ┬─┬ ノ( ゜-゜ノ)`; + Meteor.call('sendMessage', msg); +} + +slashCommands.add('unflip', Unflip, { + description: 'Slash_TableUnflip_Description', + params: 'your_message_optional', +}); diff --git a/apps/meteor/app/slashcommand-asciiarts/server/index.js b/apps/meteor/app/slashcommand-asciiarts/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommand-asciiarts/server/index.js rename to apps/meteor/app/slashcommand-asciiarts/server/index.ts diff --git a/apps/meteor/app/slashcommands-archiveroom/server/server.ts b/apps/meteor/app/slashcommands-archiveroom/server/server.ts index d7349278c10c..ae5f12ac8506 100644 --- a/apps/meteor/app/slashcommands-archiveroom/server/server.ts +++ b/apps/meteor/app/slashcommands-archiveroom/server/server.ts @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Messages } from '../../models/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { api } from '../../../server/sdk/api'; import { settings } from '../../settings/server'; -function Archive(_command: 'archive', params: string, item: Record): void | Function { +function Archive(_command: 'archive', params: string, item: IMessage): void { let channel = params.trim(); let room; @@ -61,20 +62,10 @@ function Archive(_command: 'archive', params: string, item: Record): void { +function Bridge(_command: 'bridge', stringParams: string, item: IMessage): void { if (_command !== 'bridge' || !Match.test(stringParams, String)) { return; } @@ -29,15 +30,7 @@ function Bridge(_command: 'bridge', stringParams: string, item: Record): void { +function Create(_command: 'create', params: string, item: IMessage): void { function getParams(str: string): string[] { const regex = /(--(\w+))+/g; const result = []; @@ -53,16 +54,8 @@ function Create(_command: 'create', params: string, item: Record Meteor.call('createChannel', channelStr, []); } -slashCommands.add( - 'create', - Create, - { - description: 'Create_A_New_Channel', - params: '#channel', - permission: ['create-c', 'create-p'], - }, - undefined, - false, - undefined, - undefined, -); +slashCommands.add('create', Create, { + description: 'Create_A_New_Channel', + params: '#channel', + permission: ['create-c', 'create-p'], +}); diff --git a/apps/meteor/app/slashcommands-help/index.js b/apps/meteor/app/slashcommands-help/index.js deleted file mode 100644 index ca39cd0df4b1..000000000000 --- a/apps/meteor/app/slashcommands-help/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './server/index'; diff --git a/apps/meteor/app/slashcommands-help/server/index.js b/apps/meteor/app/slashcommands-help/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-help/server/index.js rename to apps/meteor/app/slashcommands-help/server/index.ts diff --git a/apps/meteor/app/slashcommands-help/server/server.js b/apps/meteor/app/slashcommands-help/server/server.js deleted file mode 100644 index d0785f5f78d3..000000000000 --- a/apps/meteor/app/slashcommands-help/server/server.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { slashCommands } from '../../utils'; -import { api } from '../../../server/sdk/api'; - -/* - * Help is a named function that will replace /join commands - * @param {Object} message - The message object - */ - -slashCommands.add( - 'help', - function Help(command, params, item) { - const user = Meteor.users.findOne(Meteor.userId()); - const keys = [ - { - Open_channel_user_search: 'Command (or Ctrl) + p OR Command (or Ctrl) + k', - }, - { - Mark_all_as_read: 'Shift (or Ctrl) + ESC', - }, - { - Edit_previous_message: 'Up Arrow', - }, - { - Move_beginning_message: 'Command (or Alt) + Left Arrow', - }, - { - Move_beginning_message: 'Command (or Alt) + Up Arrow', - }, - { - Move_end_message: 'Command (or Alt) + Right Arrow', - }, - { - Move_end_message: 'Command (or Alt) + Down Arrow', - }, - { - New_line_message_compose_input: 'Shift + Enter', - }, - ]; - keys.forEach((key) => { - api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { - msg: TAPi18n.__( - Object.keys(key)[0], - { - postProcess: 'sprintf', - sprintf: [key[Object.keys(key)[0]]], - }, - user.language, - ), - }); - }); - }, - { - description: 'Show_the_keyboard_shortcut_list', - }, -); diff --git a/apps/meteor/app/slashcommands-help/server/server.ts b/apps/meteor/app/slashcommands-help/server/server.ts new file mode 100644 index 000000000000..b1d6fea68969 --- /dev/null +++ b/apps/meteor/app/slashcommands-help/server/server.ts @@ -0,0 +1,71 @@ +import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { settings } from '../../settings/server'; +import { slashCommands } from '../../utils/lib/slashCommand'; +import { api } from '../../../server/sdk/api'; +import { Users } from '../../models/server'; + +/* + * Help is a named function that will replace /help commands + * @param {Object} message - The message object + */ + +interface IHelpCommand { + key: string; + command: string; +} + +function Help(_command: 'help', _params: string, item: IMessage): void { + const userId = Meteor.userId() as string; + const user = Users.findOneById(userId); + + const keys: IHelpCommand[] = [ + { + key: 'Open_channel_user_search', + command: 'Command (or Ctrl) + p OR Command (or Ctrl) + k', + }, + { + key: 'Mark_all_as_read', + command: 'Shift (or Ctrl) + ESC', + }, + { + key: 'Edit_previous_message', + command: 'Up Arrow', + }, + { + key: 'Move_beginning_message', + command: 'Command (or Alt) + Left Arrow', + }, + { + key: 'Move_beginning_message', + command: 'Command (or Alt) + Up Arrow', + }, + { + key: 'Move_end_message', + command: 'Command (or Alt) + Right Arrow', + }, + { + key: 'Move_end_message', + command: 'Command (or Alt) + Down Arrow', + }, + { + key: 'New_line_message_compose_input', + command: 'Shift + Enter', + }, + ]; + keys.forEach((key) => { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__(key.key, { + postProcess: 'sprintf', + sprintf: [key.command], + lng: user?.language || settings.get('Language') || 'en', + }), + }); + }); +} + +slashCommands.add('help', Help, { + description: 'Show_the_keyboard_shortcut_list', +}); diff --git a/apps/meteor/app/slashcommands-hide/client/hide.js b/apps/meteor/app/slashcommands-hide/client/hide.ts similarity index 58% rename from apps/meteor/app/slashcommands-hide/client/hide.js rename to apps/meteor/app/slashcommands-hide/client/hide.ts index a28f9a25c199..61612bf1ff8e 100644 --- a/apps/meteor/app/slashcommands-hide/client/hide.js +++ b/apps/meteor/app/slashcommands-hide/client/hide.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils'; +import { slashCommands } from '../../utils/lib/slashCommand'; slashCommands.add('hide', undefined, { description: 'Hide_room', diff --git a/apps/meteor/app/slashcommands-hide/client/index.js b/apps/meteor/app/slashcommands-hide/client/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-hide/client/index.js rename to apps/meteor/app/slashcommands-hide/client/index.ts diff --git a/apps/meteor/app/slashcommands-hide/server/hide.js b/apps/meteor/app/slashcommands-hide/server/hide.js deleted file mode 100644 index 39dd5ed7dd50..000000000000 --- a/apps/meteor/app/slashcommands-hide/server/hide.js +++ /dev/null @@ -1,70 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { Rooms, Subscriptions } from '../../models'; -import { slashCommands } from '../../utils'; -import { api } from '../../../server/sdk/api'; - -/* - * Hide is a named function that will replace /hide commands - * @param {Object} message - The message object - */ -function Hide(command, param, item) { - if (command !== 'hide' || !Match.test(param, String)) { - return; - } - const room = param.trim(); - const user = Meteor.user(); - // if there is not a param, hide the current room - let { rid } = item; - if (room !== '') { - const [strippedRoom] = room.replace(/#|@/, '').split(' '); - const [type] = room; - - const roomObject = - type === '#' - ? Rooms.findOneByName(strippedRoom) - : Rooms.findOne({ - t: 'd', - usernames: { $all: [user.username, strippedRoom] }, - }); - - if (!roomObject) { - api.broadcast('notify.ephemeralMessage', user._id, item.rid, { - msg: TAPi18n.__( - 'Channel_doesnt_exist', - { - postProcess: 'sprintf', - sprintf: [room], - }, - user.language, - ), - }); - } - - if (!Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } })) { - return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { - msg: TAPi18n.__( - 'error-logged-user-not-in-room', - { - postProcess: 'sprintf', - sprintf: [room], - }, - user.language, - ), - }); - } - rid = roomObject._id; - } - - Meteor.call('hideRoom', rid, (error) => { - if (error) { - return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { - msg: TAPi18n.__(error, null, user.language), - }); - } - }); -} - -slashCommands.add('hide', Hide, { description: 'Hide_room', params: '#room' }); diff --git a/apps/meteor/app/slashcommands-hide/server/hide.ts b/apps/meteor/app/slashcommands-hide/server/hide.ts new file mode 100644 index 000000000000..203f304c050d --- /dev/null +++ b/apps/meteor/app/slashcommands-hide/server/hide.ts @@ -0,0 +1,74 @@ +import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { settings } from '../../settings/server'; +import { Rooms, Subscriptions, Users } from '../../models/server'; +import { slashCommands } from '../../utils/server'; +import { api } from '../../../server/sdk/api'; + +/* + * Hide is a named function that will replace /hide commands + * @param {Object} message - The message object + */ + +function Hide(_command: 'hide', param: string, item: IMessage): void { + const room = param.trim(); + const userId = Meteor.userId(); + if (!userId) { + return; + } + + const user = Users.findOneById(userId); + + if (!user) { + return; + } + + const lng = user.language || settings.get('Language') || 'en'; + + // if there is not a param, hide the current room + let { rid } = item; + if (room !== '') { + const [strippedRoom] = room.replace(/#|@/, '').split(' '); + + const [type] = room; + + const roomObject = + type === '#' + ? Rooms.findOneByName(strippedRoom) + : Rooms.findOne({ + t: 'd', + usernames: { $all: [user.username, strippedRoom] }, + }); + if (!roomObject) { + api.broadcast('notify.ephemeralMessage', user._id, item.rid, { + msg: TAPi18n.__('Channel_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [room], + lng, + }), + }); + } + if (!Subscriptions.findOneByRoomIdAndUserId(roomObject._id, user._id, { fields: { _id: 1 } })) { + api.broadcast('notify.ephemeralMessage', user._id, item.rid, { + msg: TAPi18n.__('error-logged-user-not-in-room', { + postProcess: 'sprintf', + sprintf: [room], + lng, + }), + }); + return; + } + rid = roomObject._id; + } + Meteor.call('hideRoom', rid, (error: string) => { + if (error) { + return api.broadcast('notify.ephemeralMessage', user._id, item.rid, { + msg: TAPi18n.__(error, { lng }), + }); + } + }); +} + +slashCommands.add('hide', Hide, { description: 'Hide_room', params: '#room' }); diff --git a/apps/meteor/app/slashcommands-hide/server/index.js b/apps/meteor/app/slashcommands-hide/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-hide/server/index.js rename to apps/meteor/app/slashcommands-hide/server/index.ts diff --git a/apps/meteor/app/slashcommands-invite/server/server.ts b/apps/meteor/app/slashcommands-invite/server/server.ts index afe867553602..d54b319646a8 100644 --- a/apps/meteor/app/slashcommands-invite/server/server.ts +++ b/apps/meteor/app/slashcommands-invite/server/server.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IMessage } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -11,7 +12,7 @@ import { api } from '../../../server/sdk/api'; * @param {Object} message - The message object */ -function Invite(_command: 'invite', params: string, item: Record): void { +function Invite(_command: 'invite', params: string, item: IMessage): void { const usernames = params .split(/[\s,]/) .map((username) => username.replace(/(^@)|( @)/, '')) @@ -76,16 +77,8 @@ function Invite(_command: 'invite', params: string, item: Record }); } -slashCommands.add( - 'invite', - Invite, - { - description: 'Invite_user_to_join_channel', - params: '@username', - permission: 'add-user-to-joined-room', - }, - undefined, - false, - undefined, - undefined, -); +slashCommands.add('invite', Invite, { + description: 'Invite_user_to_join_channel', + params: '@username', + permission: 'add-user-to-joined-room', +}); diff --git a/apps/meteor/app/slashcommands-inviteall/client/client.js b/apps/meteor/app/slashcommands-inviteall/client/client.ts similarity index 86% rename from apps/meteor/app/slashcommands-inviteall/client/client.js rename to apps/meteor/app/slashcommands-inviteall/client/client.ts index 9805a2e36b20..d09c9d9e908d 100644 --- a/apps/meteor/app/slashcommands-inviteall/client/client.js +++ b/apps/meteor/app/slashcommands-inviteall/client/client.ts @@ -1,4 +1,4 @@ -import { slashCommands } from '../../utils'; +import { slashCommands } from '../../utils/lib/slashCommand'; slashCommands.add('invite-all-to', undefined, { description: 'Invite_user_to_join_channel_all_to', diff --git a/apps/meteor/app/slashcommands-inviteall/client/index.js b/apps/meteor/app/slashcommands-inviteall/client/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-inviteall/client/index.js rename to apps/meteor/app/slashcommands-inviteall/client/index.ts diff --git a/apps/meteor/app/slashcommands-inviteall/server/index.js b/apps/meteor/app/slashcommands-inviteall/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-inviteall/server/index.js rename to apps/meteor/app/slashcommands-inviteall/server/index.ts diff --git a/apps/meteor/app/slashcommands-inviteall/server/server.js b/apps/meteor/app/slashcommands-inviteall/server/server.ts similarity index 57% rename from apps/meteor/app/slashcommands-inviteall/server/server.js rename to apps/meteor/app/slashcommands-inviteall/server/server.ts index 5087b63988a9..a56b2c83c769 100644 --- a/apps/meteor/app/slashcommands-inviteall/server/server.js +++ b/apps/meteor/app/slashcommands-inviteall/server/server.ts @@ -2,67 +2,77 @@ * Invite is a named function that will replace /invite commands * @param {Object} message - The message object */ + import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage, ISubscription } from '@rocket.chat/core-typings'; -import { Rooms, Subscriptions } from '../../models'; -import { slashCommands } from '../../utils'; -import { settings } from '../../settings'; +import { Rooms, Subscriptions, Users } from '../../models/server'; +import { slashCommands } from '../../utils/lib/slashCommand'; +import { settings } from '../../settings/server'; import { api } from '../../../server/sdk/api'; -function inviteAll(type) { - return function inviteAll(command, params, item) { - if (!/invite\-all-(to|from)/.test(command) || !Match.test(params, String)) { +function inviteAll(type: string): typeof slashCommands.commands[string]['callback'] { + return function inviteAll(command: string, params: string, item: IMessage): void { + if (!/invite\-all-(to|from)/.test(command)) { return; } - const regexp = /#?([^\s,.:;"']+)(?=[\s,.:;"']|$)/g; - const [, channel] = regexp.exec(params.trim()); + let channel = params.trim(); + if (channel === '') { + return; + } + + channel = channel.replace('#', ''); if (!channel) { return; } const userId = Meteor.userId(); - const currentUser = Meteor.users.findOne(userId); + if (!userId) { + return; + } + + const user = Users.findOneById(userId); + const lng = user?.language || settings.get('Language') || 'en'; + const baseChannel = type === 'to' ? Rooms.findOneById(item.rid) : Rooms.findOneByName(channel); const targetChannel = type === 'from' ? Rooms.findOneById(item.rid) : Rooms.findOneByName(channel); if (!baseChannel) { - return api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__( - 'Channel_doesnt_exist', - { - postProcess: 'sprintf', - sprintf: [channel], - }, - currentUser.language, - ), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Channel_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [channel], + lng, + }), }); + return; } const cursor = Subscriptions.findByRoomIdWhenUsernameExists(baseChannel._id, { fields: { 'u.username': 1 }, }); try { - if (cursor.count() > settings.get('API_User_Limit')) { + const APIsettings = settings.get('API_User_Limit'); + if (!APIsettings) { + return; + } + if (cursor.count() > APIsettings) { throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', { method: 'addAllToRoom', }); } - const users = cursor.fetch().map((s) => s.u.username); + const users = cursor.fetch().map((s: ISubscription) => s.u.username); if (!targetChannel && ['c', 'p'].indexOf(baseChannel.t) > -1) { Meteor.call(baseChannel.t === 'c' ? 'createChannel' : 'createPrivateGroup', channel, users); api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__( - 'Channel_created', - { - postProcess: 'sprintf', - sprintf: [channel], - }, - currentUser.language, - ), + msg: TAPi18n.__('Channel_created', { + postProcess: 'sprintf', + sprintf: [channel], + lng, + }), }); } else { Meteor.call('addUsersToRoom', { @@ -70,13 +80,14 @@ function inviteAll(type) { users, }); } - return api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__('Users_added', null, currentUser.language), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Users_added', { lng }), }); - } catch (e) { + return; + } catch (e: any) { const msg = e.error === 'cant-invite-for-direct-room' ? 'Cannot_invite_users_to_direct_rooms' : e.error; api.broadcast('notify.ephemeralMessage', userId, item.rid, { - msg: TAPi18n.__(msg, null, currentUser.language), + msg: TAPi18n.__(msg, { lng }), }); } }; diff --git a/apps/meteor/app/slashcommands-join/client/client.ts b/apps/meteor/app/slashcommands-join/client/client.ts index d18ce2106d5c..3fb1ac1c949d 100644 --- a/apps/meteor/app/slashcommands-join/client/client.ts +++ b/apps/meteor/app/slashcommands-join/client/client.ts @@ -10,14 +10,11 @@ slashCommands.add( params: '#channel', permission: 'view-c-room', }, - undefined, - false, - undefined, function (err: Meteor.Error, _result: unknown, params: Record) { if (err.error === 'error-user-already-in-room') { params.cmd = 'open'; params.msg.msg = params.msg.msg.replace('join', 'open'); - return slashCommands.run('open', params.params, params.msg, undefined); + return slashCommands.run('open', params.params, params.msg, ''); } }, ); diff --git a/apps/meteor/app/slashcommands-join/server/server.ts b/apps/meteor/app/slashcommands-join/server/server.ts index cb47d4bba271..25737a969dd4 100644 --- a/apps/meteor/app/slashcommands-join/server/server.ts +++ b/apps/meteor/app/slashcommands-join/server/server.ts @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Subscriptions } from '../../models/server'; import { settings } from '../../settings/server'; import { slashCommands } from '../../utils/lib/slashCommand'; import { api } from '../../../server/sdk/api'; -function Join(_command: 'join', params: string, item: Record): void { +function Join(_command: 'join', params: string, item: IMessage): void { let channel = params.trim(); if (channel === '') { return; @@ -45,16 +46,8 @@ function Join(_command: 'join', params: string, item: Record): v Meteor.call('joinRoom', room._id); } -slashCommands.add( - 'join', - Join, - { - description: 'Join_the_given_channel', - params: '#channel', - permission: 'view-c-room', - }, - undefined, - false, - undefined, - undefined, -); +slashCommands.add('join', Join, { + description: 'Join_the_given_channel', + params: '#channel', + permission: 'view-c-room', +}); diff --git a/apps/meteor/app/slashcommands-kick/client/client.js b/apps/meteor/app/slashcommands-kick/client/client.ts similarity index 69% rename from apps/meteor/app/slashcommands-kick/client/client.js rename to apps/meteor/app/slashcommands-kick/client/client.ts index becd5a5bd9a5..f3a2b50410aa 100644 --- a/apps/meteor/app/slashcommands-kick/client/client.js +++ b/apps/meteor/app/slashcommands-kick/client/client.ts @@ -1,8 +1,8 @@ -import { slashCommands } from '../../utils'; +import { slashCommands } from '../../utils/lib/slashCommand'; slashCommands.add( 'kick', - function (command, params) { + function (_command: 'kick', params: string) { const username = params.trim(); if (username === '') { return; diff --git a/apps/meteor/app/slashcommands-kick/client/index.js b/apps/meteor/app/slashcommands-kick/client/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-kick/client/index.js rename to apps/meteor/app/slashcommands-kick/client/index.ts diff --git a/apps/meteor/app/slashcommands-kick/server/index.js b/apps/meteor/app/slashcommands-kick/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-kick/server/index.js rename to apps/meteor/app/slashcommands-kick/server/index.ts diff --git a/apps/meteor/app/slashcommands-kick/server/server.js b/apps/meteor/app/slashcommands-kick/server/server.js deleted file mode 100644 index 9336e43e67f4..000000000000 --- a/apps/meteor/app/slashcommands-kick/server/server.js +++ /dev/null @@ -1,57 +0,0 @@ -// Kick is a named function that will replace /kick commands -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { Users, Subscriptions } from '../../models'; -import { slashCommands } from '../../utils'; -import { api } from '../../../server/sdk/api'; - -const Kick = function (command, params, { rid }) { - if (command !== 'kick' || !Match.test(params, String)) { - return; - } - const username = params.trim().replace('@', ''); - if (username === '') { - return; - } - const userId = Meteor.userId(); - const user = Meteor.users.findOne(userId); - const kickedUser = Users.findOneByUsernameIgnoringCase(username); - - if (kickedUser == null) { - return api.broadcast('notify.ephemeralMessage', userId, rid, { - msg: TAPi18n.__( - 'Username_doesnt_exist', - { - postProcess: 'sprintf', - sprintf: [username], - }, - user.language, - ), - }); - } - - const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, user._id, { - fields: { _id: 1 }, - }); - if (!subscription) { - return api.broadcast('notify.ephemeralMessage', userId, rid, { - msg: TAPi18n.__( - 'Username_is_not_in_this_room', - { - postProcess: 'sprintf', - sprintf: [username], - }, - user.language, - ), - }); - } - Meteor.call('removeUserFromRoom', { rid, username }); -}; - -slashCommands.add('kick', Kick, { - description: 'Remove_someone_from_room', - params: '@username', - permission: 'remove-user', -}); diff --git a/apps/meteor/app/slashcommands-kick/server/server.ts b/apps/meteor/app/slashcommands-kick/server/server.ts new file mode 100644 index 000000000000..5a08e53fbacd --- /dev/null +++ b/apps/meteor/app/slashcommands-kick/server/server.ts @@ -0,0 +1,54 @@ +// Kick is a named function that will replace /kick commands +import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { Users, Subscriptions } from '../../models/server'; +import { settings } from '../../settings/server'; +import { slashCommands } from '../../utils/lib/slashCommand'; +import { api } from '../../../server/sdk/api'; + +const Kick = function (_command: 'kick', params: string, item: IMessage): void { + const username = params.trim().replace('@', ''); + if (username === '') { + return; + } + const userId = Meteor.userId() as string; + const user = Users.findOneById(userId); + const lng = user?.language || settings.get('Language') || 'en'; + + const kickedUser = Users.findOneByUsernameIgnoringCase(username); + + if (kickedUser == null) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [username], + lng, + }), + }); + return; + } + + const subscription = Subscriptions.findOneByRoomIdAndUserId(item.rid, userId, { + fields: { _id: 1 }, + }); + if (!subscription) { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_is_not_in_this_room', { + postProcess: 'sprintf', + sprintf: [username], + lng, + }), + }); + return; + } + const { rid } = item; + Meteor.call('removeUserFromRoom', { rid, username }); +}; + +slashCommands.add('kick', Kick, { + description: 'Remove_someone_from_room', + params: '@username', + permission: 'remove-user', +}); diff --git a/apps/meteor/app/slashcommands-leave/index.js b/apps/meteor/app/slashcommands-leave/index.js deleted file mode 100644 index ca39cd0df4b1..000000000000 --- a/apps/meteor/app/slashcommands-leave/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './server/index'; diff --git a/apps/meteor/app/slashcommands-leave/server/index.js b/apps/meteor/app/slashcommands-leave/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-leave/server/index.js rename to apps/meteor/app/slashcommands-leave/server/index.ts diff --git a/apps/meteor/app/slashcommands-leave/server/leave.js b/apps/meteor/app/slashcommands-leave/server/leave.ts similarity index 50% rename from apps/meteor/app/slashcommands-leave/server/leave.js rename to apps/meteor/app/slashcommands-leave/server/leave.ts index dec54de5d54e..21aed9574caf 100644 --- a/apps/meteor/app/slashcommands-leave/server/leave.js +++ b/apps/meteor/app/slashcommands-leave/server/leave.ts @@ -1,23 +1,27 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; -import { slashCommands } from '../../utils'; +import { slashCommands } from '../../utils/lib/slashCommand'; +import { settings } from '../../settings/server'; import { api } from '../../../server/sdk/api'; +import { Users } from '../../models/server'; /* * Leave is a named function that will replace /leave commands * @param {Object} message - The message object */ -function Leave(command, params, item) { - if (command !== 'leave' && command !== 'part') { - return; - } - +function Leave(_command: string, _params: string, item: IMessage): void { try { Meteor.call('leaveRoom', item.rid); } catch ({ error }) { - api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { - msg: TAPi18n.__(error, null, Meteor.user().language), + const userId = Meteor.userId() as string; + if (typeof error !== 'string') { + return; + } + const user = Users.findOneById(userId); + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__(error, { lng: user?.language || settings.get('Language') || 'en' }), }); } } diff --git a/apps/meteor/app/slashcommands-me/server/me.ts b/apps/meteor/app/slashcommands-me/server/me.ts index ff536c475460..300be2ad353a 100644 --- a/apps/meteor/app/slashcommands-me/server/me.ts +++ b/apps/meteor/app/slashcommands-me/server/me.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; +import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -9,7 +10,7 @@ import { slashCommands } from '../../utils/lib/slashCommand'; */ slashCommands.add( 'me', - function Me(_command: 'me', params: string, item: Record): void { + function Me(_command: 'me', params: string, item: IMessage): void { if (s.trim(params)) { const msg = item; msg.msg = `_${params}_`; @@ -20,8 +21,4 @@ slashCommands.add( description: 'Displays_action_text', params: 'your_message', }, - undefined, - false, - undefined, - undefined, ); diff --git a/apps/meteor/app/slashcommands-msg/index.js b/apps/meteor/app/slashcommands-msg/index.js deleted file mode 100644 index ca39cd0df4b1..000000000000 --- a/apps/meteor/app/slashcommands-msg/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './server/index'; diff --git a/apps/meteor/app/slashcommands-msg/server/index.js b/apps/meteor/app/slashcommands-msg/server/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-msg/server/index.js rename to apps/meteor/app/slashcommands-msg/server/index.ts diff --git a/apps/meteor/app/slashcommands-msg/server/server.js b/apps/meteor/app/slashcommands-msg/server/server.ts similarity index 55% rename from apps/meteor/app/slashcommands-msg/server/server.js rename to apps/meteor/app/slashcommands-msg/server/server.ts index ca49ff170a6f..bea98e24e2ca 100644 --- a/apps/meteor/app/slashcommands-msg/server/server.js +++ b/apps/meteor/app/slashcommands-msg/server/server.ts @@ -1,42 +1,39 @@ import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; -import { slashCommands } from '../../utils'; -import { Users } from '../../models'; +import { slashCommands } from '../../utils/lib/slashCommand'; +import { settings } from '../../settings/server'; +import { Users } from '../../models/server'; import { api } from '../../../server/sdk/api'; /* * Msg is a named function that will replace /msg commands */ -function Msg(command, params, item) { - if (command !== 'msg' || !Match.test(params, String)) { - return; - } +function Msg(_command: 'msg', params: string, item: IMessage): void { const trimmedParams = params.trim(); const separator = trimmedParams.indexOf(' '); - const user = Meteor.users.findOne(Meteor.userId()); + const userId = Meteor.userId() as string; if (separator === -1) { - return api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { - msg: TAPi18n.__('Username_and_message_must_not_be_empty', null, user.language), + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_and_message_must_not_be_empty', { lng: settings.get('Language') || 'en' }), }); + return; } const message = trimmedParams.slice(separator + 1); const targetUsernameOrig = trimmedParams.slice(0, separator); const targetUsername = targetUsernameOrig.replace('@', ''); const targetUser = Users.findOneByUsernameIgnoringCase(targetUsername); if (targetUser == null) { - api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { - msg: TAPi18n.__( - 'Username_doesnt_exist', - { - postProcess: 'sprintf', - sprintf: [targetUsernameOrig], - }, - user.language, - ), + const user = Users.findOneById(userId, { fields: { language: 1 } }); + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('Username_doesnt_exist', { + postProcess: 'sprintf', + sprintf: [targetUsernameOrig], + lng: user?.language || settings.get('Language') || 'en', + }), }); return; } diff --git a/apps/meteor/app/slashcommands-mute/index.ts b/apps/meteor/app/slashcommands-mute/index.ts deleted file mode 100644 index ca39cd0df4b1..000000000000 --- a/apps/meteor/app/slashcommands-mute/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './server/index'; diff --git a/apps/meteor/app/slashcommands-mute/server/mute.ts b/apps/meteor/app/slashcommands-mute/server/mute.ts index ee000c5667f3..ed4fd5312893 100644 --- a/apps/meteor/app/slashcommands-mute/server/mute.ts +++ b/apps/meteor/app/slashcommands-mute/server/mute.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { settings } from '../../settings/server'; @@ -10,7 +11,7 @@ import { api } from '../../../server/sdk/api'; * Mute is a named function that will replace /mute commands */ -function Mute(_command: 'mute', params: string, item: Record): void { +function Mute(_command: 'mute', params: string, item: IMessage): void { const username = params.trim().replace('@', ''); if (username === '') { return; @@ -46,16 +47,8 @@ function Mute(_command: 'mute', params: string, item: Record): v }); } -slashCommands.add( - 'mute', - Mute, - { - description: 'Mute_someone_in_room', - params: '@username', - permission: 'mute-user', - }, - undefined, - false, - undefined, - undefined, -); +slashCommands.add('mute', Mute, { + description: 'Mute_someone_in_room', + params: '@username', + permission: 'mute-user', +}); diff --git a/apps/meteor/app/slashcommands-mute/server/unmute.ts b/apps/meteor/app/slashcommands-mute/server/unmute.ts index bcc43f1b1f10..38e9229b95d0 100644 --- a/apps/meteor/app/slashcommands-mute/server/unmute.ts +++ b/apps/meteor/app/slashcommands-mute/server/unmute.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IMessage } from '@rocket.chat/core-typings'; import { slashCommands } from '../../utils/lib/slashCommand'; import { Users, Subscriptions } from '../../models/server'; @@ -10,7 +11,7 @@ import { api } from '../../../server/sdk/api'; * Unmute is a named function that will replace /unmute commands */ -function Unmute(_command: 'unmute', params: string, item: Record): void | Promise { +function Unmute(_command: 'unmute', params: string, item: IMessage): void | Promise { const username = params.trim().replace('@', ''); if (username === '') { return; @@ -45,16 +46,8 @@ function Unmute(_command: 'unmute', params: string, item: Record }); } -slashCommands.add( - 'unmute', - Unmute, - { - description: 'Unmute_someone_in_room', - params: '@username', - permission: 'mute-user', - }, - undefined, - false, - undefined, - undefined, -); +slashCommands.add('unmute', Unmute, { + description: 'Unmute_someone_in_room', + params: '@username', + permission: 'mute-user', +}); diff --git a/apps/meteor/app/slashcommands-open/client/client.js b/apps/meteor/app/slashcommands-open/client/client.ts similarity index 63% rename from apps/meteor/app/slashcommands-open/client/client.js rename to apps/meteor/app/slashcommands-open/client/client.ts index 9d630bddc83a..ce0342b20c6b 100644 --- a/apps/meteor/app/slashcommands-open/client/client.js +++ b/apps/meteor/app/slashcommands-open/client/client.ts @@ -1,35 +1,25 @@ import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; import { FlowRouter } from 'meteor/kadira:flow-router'; +import type { IMessage } from '@rocket.chat/core-typings'; -import { slashCommands } from '../../utils'; import { roomCoordinator } from '../../../client/lib/rooms/roomCoordinator'; -import { ChatSubscription, Subscriptions } from '../../models'; +import { slashCommands } from '../../utils/lib/slashCommand'; +import { Subscriptions, ChatSubscription } from '../../models/client'; -function Open(command, params /* , item*/) { - const dict = { +function Open(_command: 'open', params: string, _item: IMessage): void { + const dict: Record = { '#': ['c', 'p'], '@': ['d'], }; - if (command !== 'open' || !Match.test(params, String)) { - return; - } - - let room = params.trim(); - const type = dict[room[0]]; - room = room.replace(/#|@/, ''); + const room = params.trim().replace(/#|@/, ''); + const type = dict[params.trim()[0]] || []; const query = { name: room, + ...(type && { t: { $in: type } }), }; - if (type) { - query.t = { - $in: type, - }; - } - const subscription = ChatSubscription.findOne(query); if (subscription) { @@ -39,7 +29,7 @@ function Open(command, params /* , item*/) { if (type && type.indexOf('d') === -1) { return; } - return Meteor.call('createDirectMessage', room, function (err) { + return Meteor.call('createDirectMessage', room, function (err: Meteor.Error) { if (err) { return; } diff --git a/apps/meteor/app/slashcommands-open/client/index.js b/apps/meteor/app/slashcommands-open/client/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-open/client/index.js rename to apps/meteor/app/slashcommands-open/client/index.ts diff --git a/apps/meteor/app/slashcommands-open/index.js b/apps/meteor/app/slashcommands-open/index.ts similarity index 100% rename from apps/meteor/app/slashcommands-open/index.js rename to apps/meteor/app/slashcommands-open/index.ts diff --git a/apps/meteor/app/slashcommands-status/client/index.js b/apps/meteor/app/slashcommands-status/client/index.js deleted file mode 100644 index 11e5ad1b8640..000000000000 --- a/apps/meteor/app/slashcommands-status/client/index.js +++ /dev/null @@ -1 +0,0 @@ -import '../lib/status'; diff --git a/apps/meteor/app/slashcommands-status/client/index.ts b/apps/meteor/app/slashcommands-status/client/index.ts new file mode 100644 index 000000000000..57ce4c92d8f8 --- /dev/null +++ b/apps/meteor/app/slashcommands-status/client/index.ts @@ -0,0 +1 @@ +import './status'; diff --git a/apps/meteor/app/slashcommands-status/client/status.ts b/apps/meteor/app/slashcommands-status/client/status.ts new file mode 100644 index 000000000000..69b1dd11241e --- /dev/null +++ b/apps/meteor/app/slashcommands-status/client/status.ts @@ -0,0 +1,26 @@ +import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +import { settings } from '../../settings/server'; +import { api } from '../../../server/sdk/api'; +import { handleError } from '../../../client/lib/utils/handleError'; + +function Status(_command: 'status', params: string, item: IMessage): void { + const userId = Meteor.userId() as string; + + Meteor.call('setUserStatus', null, params, (err: Meteor.Error) => { + if (err) { + return handleError(err); + } + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('StatusMessage_Changed_Successfully', { lng: settings.get('Language') || 'en' }), + }); + }); +} + +slashCommands.add('status', Status, { + description: 'Slash_Status_Description', + params: 'Slash_Status_Params', +}); diff --git a/apps/meteor/app/slashcommands-status/index.js b/apps/meteor/app/slashcommands-status/index.js deleted file mode 100644 index a67eca871efb..000000000000 --- a/apps/meteor/app/slashcommands-status/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -if (Meteor.isClient) { - module.exports = require('./client/index.js'); -} -if (Meteor.isServer) { - module.exports = require('./server/index.js'); -} diff --git a/apps/meteor/app/slashcommands-status/lib/status.js b/apps/meteor/app/slashcommands-status/lib/status.js deleted file mode 100644 index 359068a0cda5..000000000000 --- a/apps/meteor/app/slashcommands-status/lib/status.js +++ /dev/null @@ -1,37 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { slashCommands } from '../../utils'; -import { api } from '../../../server/sdk/api'; - -function Status(command, params, item) { - if (command === 'status') { - const user = Meteor.users.findOne(Meteor.userId()); - - Meteor.call('setUserStatus', null, params, (err) => { - if (err) { - if (Meteor.isClient) { - const { handleError } = require('../../../client/lib/utils/handleError'); - return handleError(err); - } - - if (err.error === 'error-not-allowed') { - api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { - msg: TAPi18n.__('StatusMessage_Change_Disabled', null, user.language), - }); - } - - throw err; - } else { - api.broadcast('notify.ephemeralMessage', Meteor.userId(), item.rid, { - msg: TAPi18n.__('StatusMessage_Changed_Successfully', null, user.language), - }); - } - }); - } -} - -slashCommands.add('status', Status, { - description: 'Slash_Status_Description', - params: 'Slash_Status_Params', -}); diff --git a/apps/meteor/app/slashcommands-status/server/index.js b/apps/meteor/app/slashcommands-status/server/index.js deleted file mode 100644 index 11e5ad1b8640..000000000000 --- a/apps/meteor/app/slashcommands-status/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import '../lib/status'; diff --git a/apps/meteor/app/slashcommands-status/server/index.ts b/apps/meteor/app/slashcommands-status/server/index.ts new file mode 100644 index 000000000000..57ce4c92d8f8 --- /dev/null +++ b/apps/meteor/app/slashcommands-status/server/index.ts @@ -0,0 +1 @@ +import './status'; diff --git a/apps/meteor/app/slashcommands-status/server/status.ts b/apps/meteor/app/slashcommands-status/server/status.ts new file mode 100644 index 000000000000..5bbee67baf8c --- /dev/null +++ b/apps/meteor/app/slashcommands-status/server/status.ts @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +import { settings } from '../../settings/server'; +import { api } from '../../../server/sdk/api'; +import { Users } from '../../models/server'; + +function Status(_command: 'status', params: string, item: IMessage): void { + const userId = Meteor.userId() as string; + + Meteor.call('setUserStatus', null, params, (err: Meteor.Error) => { + const user = userId && Users.findOneById(userId, { fields: { language: 1 } }); + const lng = user?.language || settings.get('Language') || 'en'; + + if (err) { + if (err.error === 'error-not-allowed') { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('StatusMessage_Change_Disabled', { lng }), + }); + } + + throw err; + } else { + api.broadcast('notify.ephemeralMessage', userId, item.rid, { + msg: TAPi18n.__('StatusMessage_Changed_Successfully', { lng }), + }); + } + }); +} + +slashCommands.add('status', Status, { + description: 'Slash_Status_Description', + params: 'Slash_Status_Params', +}); diff --git a/apps/meteor/app/slashcommands-topic/client/index.js b/apps/meteor/app/slashcommands-topic/client/index.js deleted file mode 100644 index 9ea49d4c9e63..000000000000 --- a/apps/meteor/app/slashcommands-topic/client/index.js +++ /dev/null @@ -1 +0,0 @@ -import '../lib/topic'; diff --git a/apps/meteor/app/slashcommands-topic/client/index.ts b/apps/meteor/app/slashcommands-topic/client/index.ts new file mode 100644 index 000000000000..2ad19c172e54 --- /dev/null +++ b/apps/meteor/app/slashcommands-topic/client/index.ts @@ -0,0 +1 @@ +import './topic'; diff --git a/apps/meteor/app/slashcommands-topic/client/topic.ts b/apps/meteor/app/slashcommands-topic/client/topic.ts new file mode 100644 index 000000000000..8b0e121ee013 --- /dev/null +++ b/apps/meteor/app/slashcommands-topic/client/topic.ts @@ -0,0 +1,31 @@ +import { Meteor } from 'meteor/meteor'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +import { ChatRoom } from '../../models/client/models/ChatRoom'; +import { callbacks } from '../../../lib/callbacks'; +import { hasPermission } from '../../authorization/client'; +import { handleError } from '../../../client/lib/utils/handleError'; + +function Topic(_command: 'topic', params: string, item: IMessage): void { + if (Meteor.isClient && hasPermission('edit-room', item.rid)) { + Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err: Meteor.Error) => { + if (err) { + if (Meteor.isClient) { + handleError(err); + } + throw err; + } + + if (Meteor.isClient) { + callbacks.run('roomTopicChanged', ChatRoom.findOne(item.rid)); + } + }); + } +} + +slashCommands.add('topic', Topic, { + description: 'Slash_Topic_Description', + params: 'Slash_Topic_Params', + permission: 'edit-room', +}); diff --git a/apps/meteor/app/slashcommands-topic/lib/topic.js b/apps/meteor/app/slashcommands-topic/lib/topic.js deleted file mode 100644 index 24685fc4d309..000000000000 --- a/apps/meteor/app/slashcommands-topic/lib/topic.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { slashCommands } from '../../utils'; -import { ChatRoom } from '../../models'; -import { callbacks } from '../../../lib/callbacks'; -import { hasPermission } from '../../authorization'; - -function Topic(command, params, item) { - if (command === 'topic') { - if ( - (Meteor.isClient && hasPermission('edit-room', item.rid)) || - (Meteor.isServer && hasPermission(Meteor.userId(), 'edit-room', item.rid)) - ) { - Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err) => { - if (err) { - if (Meteor.isClient) { - const { handleError } = require('../../../client/lib/utils/handleError'); - return handleError(err); - } - throw err; - } - - if (Meteor.isClient) { - callbacks.run('roomTopicChanged', ChatRoom.findOne(item.rid)); - } - }); - } - } -} - -slashCommands.add('topic', Topic, { - description: 'Slash_Topic_Description', - params: 'Slash_Topic_Params', - permission: 'edit-room', -}); diff --git a/apps/meteor/app/slashcommands-topic/server/index.js b/apps/meteor/app/slashcommands-topic/server/index.js deleted file mode 100644 index 9ea49d4c9e63..000000000000 --- a/apps/meteor/app/slashcommands-topic/server/index.js +++ /dev/null @@ -1 +0,0 @@ -import '../lib/topic'; diff --git a/apps/meteor/app/slashcommands-topic/server/index.ts b/apps/meteor/app/slashcommands-topic/server/index.ts new file mode 100644 index 000000000000..2ad19c172e54 --- /dev/null +++ b/apps/meteor/app/slashcommands-topic/server/index.ts @@ -0,0 +1 @@ +import './topic'; diff --git a/apps/meteor/app/slashcommands-topic/server/topic.ts b/apps/meteor/app/slashcommands-topic/server/topic.ts new file mode 100644 index 000000000000..763861d2d20b --- /dev/null +++ b/apps/meteor/app/slashcommands-topic/server/topic.ts @@ -0,0 +1,21 @@ +import { Meteor } from 'meteor/meteor'; +import type { IMessage } from '@rocket.chat/core-typings'; + +import { slashCommands } from '../../utils/lib/slashCommand'; +import { hasPermission } from '../../authorization/server/functions/hasPermission'; + +function Topic(_command: 'topic', params: string, item: IMessage): void { + if (Meteor.isServer && hasPermission(Meteor.userId() as string, 'edit-room', item.rid)) { + Meteor.call('saveRoomSettings', item.rid, 'roomTopic', params, (err: Meteor.Error) => { + if (err) { + throw err; + } + }); + } +} + +slashCommands.add('topic', Topic, { + description: 'Slash_Topic_Description', + params: 'Slash_Topic_Params', + permission: 'edit-room', +}); diff --git a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts index c89efd2ad8d5..ad3c999f7b38 100644 --- a/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts +++ b/apps/meteor/app/slashcommands-unarchiveroom/server/server.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { IMessage } from '@rocket.chat/core-typings'; import { Rooms, Messages } from '../../models/server'; import { slashCommands } from '../../utils/lib/slashCommand'; @@ -8,7 +9,7 @@ import { api } from '../../../server/sdk/api'; import { roomCoordinator } from '../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../definition/IRoomTypeConfig'; -function Unarchive(_command: 'unarchive', params: string, item: Record): void | Promise | Function { +function Unarchive(_command: 'unarchive', params: string, item: IMessage): void { let channel = params.trim(); let room; @@ -23,13 +24,14 @@ function Unarchive(_command: 'unarchive', params: string, item: Record { + return { + totalCSVImportedUsers: settings.get('CSV_Importer_Count'), + totalHipchatEnterpriseImportedUsers: settings.get('Hipchat_Enterprise_Importer_Count'), + totalSlackImportedUsers: settings.get('Slack_Importer_Count'), + totalSlackUsersImportedUsers: settings.get('Slack_Users_Importer_Count'), + }; +} diff --git a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts index b41cbb2d8385..b0aec1438019 100644 --- a/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts +++ b/apps/meteor/app/statistics/server/lib/getServicesStatistics.ts @@ -1,6 +1,11 @@ +import { MongoInternals } from 'meteor/mongo'; + +import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; + function getCustomOAuthServices(): Record< string, { @@ -9,6 +14,8 @@ function getCustomOAuthServices(): Record< users: number; } > { + const readPreference = readSecondaryPreferred(db); + const customOauth = settings.getByRegexp(/Accounts_OAuth_Custom-[^-]+$/im); return Object.fromEntries( Object.entries(customOauth).map(([key, value]) => { @@ -18,7 +25,7 @@ function getCustomOAuthServices(): Record< { enabled: Boolean(value), mergeRoles: settings.get(`Accounts_OAuth_Custom-${name}-merge_roles`), - users: Users.countActiveUsersByService(name), + users: Users.countActiveUsersByService(name, { readPreference }), }, ]; }), @@ -26,9 +33,11 @@ function getCustomOAuthServices(): Record< } export function getServicesStatistics(): Record { + const readPreference = readSecondaryPreferred(db); + return { ldap: { - users: Users.countActiveUsersByService('ldap'), + users: Users.countActiveUsersByService('ldap', { readPreference }), enabled: settings.get('LDAP_Enable'), loginFallback: settings.get('LDAP_Login_Fallback'), encryption: settings.get('LDAP_Encryption'), @@ -53,7 +62,7 @@ export function getServicesStatistics(): Record { }, saml: { enabled: settings.get('SAML_Custom_Default'), - users: Users.countActiveUsersByService('saml'), + users: Users.countActiveUsersByService('saml', { readPreference }), signatureValidationType: settings.get('SAML_Custom_Default_signature_validation_type'), generateUsername: settings.get('SAML_Custom_Default_generate_username'), updateSubscriptionsOnLogin: settings.get('SAML_Custom_Default_channels_update'), @@ -61,66 +70,66 @@ export function getServicesStatistics(): Record { }, cas: { enabled: settings.get('CAS_enabled'), - users: Users.countActiveUsersByService('cas'), + users: Users.countActiveUsersByService('cas', { readPreference }), allowUserCreation: settings.get('CAS_Creation_User_Enabled'), alwaysSyncUserData: settings.get('CAS_Sync_User_Data_Enabled'), }, oauth: { apple: { enabled: settings.get('Accounts_OAuth_Apple'), - users: Users.countActiveUsersByService('apple'), + users: Users.countActiveUsersByService('apple', { readPreference }), }, dolphin: { enabled: settings.get('Accounts_OAuth_Dolphin'), - users: Users.countActiveUsersByService('dolphin'), + users: Users.countActiveUsersByService('dolphin', { readPreference }), }, drupal: { enabled: settings.get('Accounts_OAuth_Drupal'), - users: Users.countActiveUsersByService('drupal'), + users: Users.countActiveUsersByService('drupal', { readPreference }), }, facebook: { enabled: settings.get('Accounts_OAuth_Facebook'), - users: Users.countActiveUsersByService('facebook'), + users: Users.countActiveUsersByService('facebook', { readPreference }), }, github: { enabled: settings.get('Accounts_OAuth_Github'), - users: Users.countActiveUsersByService('github'), + users: Users.countActiveUsersByService('github', { readPreference }), }, githubEnterprise: { enabled: settings.get('Accounts_OAuth_GitHub_Enterprise'), - users: Users.countActiveUsersByService('github_enterprise'), + users: Users.countActiveUsersByService('github_enterprise', { readPreference }), }, gitlab: { enabled: settings.get('Accounts_OAuth_Gitlab'), - users: Users.countActiveUsersByService('gitlab'), + users: Users.countActiveUsersByService('gitlab', { readPreference }), }, google: { enabled: settings.get('Accounts_OAuth_Google'), - users: Users.countActiveUsersByService('google'), + users: Users.countActiveUsersByService('google', { readPreference }), }, linkedin: { enabled: settings.get('Accounts_OAuth_Linkedin'), - users: Users.countActiveUsersByService('linkedin'), + users: Users.countActiveUsersByService('linkedin', { readPreference }), }, meteor: { enabled: settings.get('Accounts_OAuth_Meteor'), - users: Users.countActiveUsersByService('meteor'), + users: Users.countActiveUsersByService('meteor', { readPreference }), }, nextcloud: { enabled: settings.get('Accounts_OAuth_Nextcloud'), - users: Users.countActiveUsersByService('nextcloud'), + users: Users.countActiveUsersByService('nextcloud', { readPreference }), }, tokenpass: { enabled: settings.get('Accounts_OAuth_Tokenpass'), - users: Users.countActiveUsersByService('tokenpass'), + users: Users.countActiveUsersByService('tokenpass', { readPreference }), }, twitter: { enabled: settings.get('Accounts_OAuth_Twitter'), - users: Users.countActiveUsersByService('twitter'), + users: Users.countActiveUsersByService('twitter', { readPreference }), }, wordpress: { enabled: settings.get('Accounts_OAuth_Wordpress'), - users: Users.countActiveUsersByService('wordpress'), + users: Users.countActiveUsersByService('wordpress', { readPreference }), }, custom: getCustomOAuthServices(), }, diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 9471d1d1c1eb..051456c55ae7 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -18,6 +18,7 @@ import { Statistics, Sessions, Integrations, + Invites, Uploads, LivechatDepartment, EmailInbox, @@ -27,9 +28,10 @@ import { } from '../../../models/server/raw'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { getAppsStatistics } from './getAppsStatistics'; +import { getImporterStatistics } from './getImporterStatistics'; import { getServicesStatistics } from './getServicesStatistics'; import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server'; -import { Analytics } from '../../../../server/sdk'; +import { Analytics, Team } from '../../../../server/sdk'; import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics'; const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server']; @@ -395,6 +397,7 @@ export const statistics = { statistics.apps = getAppsStatistics(); statistics.services = getServicesStatistics(); + statistics.importer = getImporterStatistics(); // If getSettingsStatistics() returns an error, save as empty object. statsPms.push( @@ -448,6 +451,12 @@ export const statistics = { }), ); + statsPms.push( + Team.getStatistics().then((result) => { + statistics.teams = result; + }), + ); + statsPms.push(Analytics.resetSeatRequestCount()); statistics.dashboardCount = settings.get('Engagement_Dashboard_Load_Count'); @@ -457,6 +466,21 @@ export const statistics = { statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); statistics.totalOTRRooms = Rooms.findByCreatedOTR().count(); statistics.totalOTR = settings.get('OTR_Count'); + statistics.totalRoomsWithStarred = await MessagesRaw.countRoomsWithStarredMessages({ readPreference }); + statistics.totalRoomsWithPinned = await MessagesRaw.countRoomsWithPinnedMessages({ readPreference }); + statistics.totalUserTOTP = await UsersRaw.findActiveUsersTOTPEnable({ readPreference }).count(); + statistics.totalUserEmail2fa = await UsersRaw.findActiveUsersEmail2faEnable({ readPreference }).count(); + statistics.totalPinned = await MessagesRaw.findPinned({ readPreference }).count(); + statistics.totalStarred = await MessagesRaw.findStarred({ readPreference }).count(); + statistics.totalLinkInvitation = await Invites.find().count(); + statistics.totalLinkInvitationUses = await Invites.countUses(); + statistics.totalEmailInvitation = settings.get('Invitation_Email_Count'); + statistics.totalE2ERooms = await RoomsRaw.findByE2E({ readPreference }).count(); + statistics.logoChange = Object.keys(settings.get('Assets_logo')).includes('url'); + statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== 'Home'; + statistics.showHomeButton = settings.get('Layout_Show_Home_Button'); + statistics.totalEncryptedMessages = await MessagesRaw.countE2EEMessages({ readPreference }); + statistics.totalManuallyAddedUsers = settings.get('Manual_Entry_User_Count'); await Promise.all(statsPms).catch(log); diff --git a/apps/meteor/app/theme/client/imports/general/base_old.css b/apps/meteor/app/theme/client/imports/general/base_old.css index fcaa51a66be0..5e198fed7f87 100644 --- a/apps/meteor/app/theme/client/imports/general/base_old.css +++ b/apps/meteor/app/theme/client/imports/general/base_old.css @@ -1661,7 +1661,7 @@ padding-bottom: 24px; } - & ul { + & ul.messages-list { padding: 21px 0 10px; } diff --git a/apps/meteor/app/threads/server/methods/followMessage.js b/apps/meteor/app/threads/server/methods/followMessage.js index b591a68f3a1b..99df51f65462 100644 --- a/apps/meteor/app/threads/server/methods/followMessage.js +++ b/apps/meteor/app/threads/server/methods/followMessage.js @@ -6,6 +6,7 @@ import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; import { canAccessRoomId } from '../../../authorization/server'; import { follow } from '../functions'; +import { Apps, AppEvents } from '../../../apps/server/orchestrator'; Meteor.methods({ followMessage({ mid }) { @@ -20,7 +21,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); } - const message = Messages.findOneById(mid, { fields: { rid: 1, tmid: 1 } }); + const message = Messages.findOneById(mid); if (!message) { throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage', @@ -31,7 +32,12 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); } - return follow({ tmid: message.tmid || message._id, uid }); + const followResult = follow({ tmid: message.tmid || message._id, uid }); + + const isFollowed = true; + Promise.await(Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, Meteor.user(), isFollowed)); + + return followResult; }, }); diff --git a/apps/meteor/app/threads/server/methods/unfollowMessage.js b/apps/meteor/app/threads/server/methods/unfollowMessage.js index e0f9189b386c..5381fd925d38 100644 --- a/apps/meteor/app/threads/server/methods/unfollowMessage.js +++ b/apps/meteor/app/threads/server/methods/unfollowMessage.js @@ -6,6 +6,7 @@ import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; import { canAccessRoomId } from '../../../authorization/server'; import { unfollow } from '../functions'; +import { Apps, AppEvents } from '../../../apps/server/orchestrator'; Meteor.methods({ unfollowMessage({ mid }) { @@ -20,7 +21,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' }); } - const message = Messages.findOneById(mid, { fields: { rid: 1, tmid: 1 } }); + const message = Messages.findOneById(mid); if (!message) { throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'unfollowMessage', @@ -31,7 +32,12 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' }); } - return unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); + const unfollowResult = unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); + + const isFollowed = false; + Promise.await(Apps.triggerEvent(AppEvents.IPostMessageFollowed, message, Meteor.user(), isFollowed)); + + return unfollowResult; }, }); diff --git a/apps/meteor/app/ui/client/views/app/photoswipeContent.ts b/apps/meteor/app/ui/client/views/app/photoswipeContent.ts index 16359e2b2f82..bc2af32b53dc 100644 --- a/apps/meteor/app/ui/client/views/app/photoswipeContent.ts +++ b/apps/meteor/app/ui/client/views/app/photoswipeContent.ts @@ -3,7 +3,7 @@ import { Blaze } from 'meteor/blaze'; import { Template } from 'meteor/templating'; import { escapeHTML } from '@rocket.chat/string-helpers'; import type PhotoSwipe from 'photoswipe'; -import type PhotoSwipeUiDefault from 'photoswipe/dist/photoswipe-ui-default'; +import PhotoSwipeUIDefault from 'photoswipe/dist/photoswipe-ui-default'; const parseLength = (x: unknown): number | undefined => { const length = typeof x === 'string' ? parseInt(x, 10) : undefined; @@ -25,7 +25,7 @@ const getImageSize = (src: string): Promise<[w: number, h: number]> => img.src = src; }); -type Slide = PhotoSwipeUiDefault.Item & { description?: string }; +type Slide = PhotoSwipe.Item & { description?: string; title?: string }; const fromElementToSlide = async (element: Element): Promise => { if (!(element instanceof HTMLElement)) { @@ -70,15 +70,11 @@ const fromElementToSlide = async (element: Element): Promise => { return null; }; -let currentGallery: PhotoSwipe | null = null; +let currentGallery: PhotoSwipe | null = null; -const initGallery = async (items: Slide[], options: PhotoSwipeUiDefault.Options): Promise => { - const [ - { default: PhotoSwipe }, - { default: PhotoSwipeUiDefault }, // eslint-disable-line @typescript-eslint/camelcase - ] = await Promise.all([ +const initGallery = async (items: Slide[], options: PhotoSwipe.Options): Promise => { + const [{ default: PhotoSwipe }] = await Promise.all([ import('photoswipe'), - import('photoswipe/dist/photoswipe-ui-default'), // @ts-ignore import('photoswipe/dist/photoswipe.css'), // @ts-ignore @@ -94,7 +90,7 @@ const initGallery = async (items: Slide[], options: PhotoSwipeUiDefault.Options) throw new Error('Photoswipe container element not found'); } - currentGallery = new PhotoSwipe(container, PhotoSwipeUiDefault, items, options); + currentGallery = new PhotoSwipe(container, PhotoSwipeUIDefault, items, options); currentGallery.listen('destroy', () => { currentGallery = null; @@ -104,13 +100,11 @@ const initGallery = async (items: Slide[], options: PhotoSwipeUiDefault.Options) } }; -const defaultGalleryOptions: PhotoSwipeUiDefault.Options = { +const defaultGalleryOptions = { bgOpacity: 0.7, - showHideOpacity: true, - counterEl: false, - shareEl: false, - clickToCloseNonZoomable: false, index: 0, + wheelToZoom: true, + padding: { top: 20, bottom: 40, left: 100, right: 100 }, addCaptionHTMLFn(item: Slide, captionEl: HTMLElement): boolean { captionEl.children[0].innerHTML = ` ${escapeHTML(item.title ?? '')}
diff --git a/apps/meteor/app/ui/client/views/app/room.html b/apps/meteor/app/ui/client/views/app/room.html index cfd07e7c846c..ee5c2c3a951d 100644 --- a/apps/meteor/app/ui/client/views/app/room.html +++ b/apps/meteor/app/ui/client/views/app/room.html @@ -77,7 +77,7 @@ {{/with}}
-
    +
      {{#if canPreview}} {{#if hasMore}}
    • diff --git a/apps/meteor/app/utils/lib/slashCommand.d.ts b/apps/meteor/app/utils/lib/slashCommand.d.ts deleted file mode 100644 index a21676baaf98..000000000000 --- a/apps/meteor/app/utils/lib/slashCommand.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -export declare const slashCommands: { - commands: { [key: string]: any }; - add( - command: string, - callback: Function | undefined, - options: object, - result: unknown | undefined, - providesPreview: boolean, - previewer: unknown | undefined, - previewCallback: Function | undefined, - ): void; - run(command: string, params: string, message: object, triggerId: string | undefined): Function | void; - getPreviews(command: string, params: string, message: object, preview?: unknown, triggerId?: string | undefined): Function | void; - executePreview(command: string, params: string, message: object, preview: unknown, triggerId: string | undefined): Function | void; -}; diff --git a/apps/meteor/app/utils/lib/slashCommand.js b/apps/meteor/app/utils/lib/slashCommand.js deleted file mode 100644 index 42d9dd5a5365..000000000000 --- a/apps/meteor/app/utils/lib/slashCommand.js +++ /dev/null @@ -1,79 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -export const slashCommands = { - commands: {}, - add: function _addingSlashCommand(command, callback, options = {}, result, providesPreview = false, previewer, previewCallback) { - slashCommands.commands[command] = { - command, - callback, - params: options.params, - description: options.description, - permission: options.permission, - clientOnly: options.clientOnly || false, - result, - providesPreview, - previewer, - previewCallback, - }; - }, - run: function _runningSlashCommand(command, params, message, triggerId) { - if (slashCommands.commands[command] && typeof slashCommands.commands[command].callback === 'function') { - if (!message || !message.rid) { - throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); - } - return slashCommands.commands[command].callback(command, params, message, triggerId); - } - }, - getPreviews: function _gettingSlashCommandPreviews(command, params, message) { - if (slashCommands.commands[command] && typeof slashCommands.commands[command].previewer === 'function') { - if (!message || !message.rid) { - throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); - } - - // { i18nTitle, items: [{ id, type, value }] } - const previewInfo = slashCommands.commands[command].previewer(command, params, message); - - if (typeof previewInfo !== 'object' || !Array.isArray(previewInfo.items) || previewInfo.items.length === 0) { - return; - } - - // A limit of ten results, to save time and bandwidth - if (previewInfo.items.length >= 10) { - previewInfo.items = previewInfo.items.slice(0, 10); - } - - return previewInfo; - } - }, - executePreview: function _executeSlashCommandPreview(command, params, message, preview, triggerId) { - if (slashCommands.commands[command] && typeof slashCommands.commands[command].previewCallback === 'function') { - if (!message || !message.rid) { - throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); - } - - // { id, type, value } - if (!preview.id || !preview.type || !preview.value) { - throw new Meteor.Error('error-invalid-preview', 'Preview Item must have an id, type, and value.'); - } - - return slashCommands.commands[command].previewCallback(command, params, message, preview, triggerId); - } - }, -}; - -Meteor.methods({ - slashCommand(command) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'slashCommand', - }); - } - - if (!command || !command.cmd || !slashCommands.commands[command.cmd]) { - throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', { - method: 'executeSlashCommandPreview', - }); - } - return slashCommands.run(command.cmd, command.params, command.msg, command.triggerId); - }, -}); diff --git a/apps/meteor/app/utils/lib/slashCommand.ts b/apps/meteor/app/utils/lib/slashCommand.ts new file mode 100644 index 000000000000..ef8175d9c5d0 --- /dev/null +++ b/apps/meteor/app/utils/lib/slashCommand.ts @@ -0,0 +1,140 @@ +import { Meteor } from 'meteor/meteor'; +import type { IMessage } from '@rocket.chat/core-typings'; + +type SlashCommandCallback = (command: T, params: string, message: IMessage, triggerId: string) => void; + +type SlashCommandPreviewItem = { + id: string; + type: 'image' | 'video' | 'audio' | 'text' | 'other'; + value: string; +}; + +type SlashCommandPreviews = { + i18nTitle: string; + items: SlashCommandPreviewItem[]; +}; + +type SlashCommandPreviewer = (command: string, params: string, message: IMessage) => SlashCommandPreviews | undefined; + +type SlashCommandPreviewCallback = ( + command: string, + params: string, + message: IMessage, + preview: SlashCommandPreviewItem, + triggerId: string, +) => void; + +type SlashCommandOptions = { + params?: string; + description?: string; + permission?: string | string[]; + clientOnly?: boolean; +}; + +type SlashCommand = { + command: T; + callback?: SlashCommandCallback; + params: SlashCommandOptions['params']; + description: SlashCommandOptions['description']; + permission: SlashCommandOptions['permission']; + clientOnly?: SlashCommandOptions['clientOnly']; + result?: (err: Meteor.Error, result: never, data: { cmd: T; params: string; msg: IMessage }) => void; + providesPreview: boolean; + previewer?: SlashCommandPreviewer; + previewCallback?: SlashCommandPreviewCallback; +}; + +export const slashCommands = { + commands: {} as Record>, + add( + command: T, + callback?: SlashCommand['callback'], + options: SlashCommandOptions = {}, + result?: SlashCommand['result'], + providesPreview = false, + previewer?: SlashCommand['previewer'], + previewCallback?: SlashCommand['previewCallback'], + ): void { + this.commands[command] = { + command, + callback, + params: options.params, + description: options.description, + permission: options.permission, + clientOnly: options.clientOnly || false, + result, + providesPreview, + previewer, + previewCallback, + } as SlashCommand; + }, + run(command: string, params: string, message: IMessage, triggerId: string): void { + const cmd = this.commands[command]; + if (typeof cmd?.callback !== 'function') { + return; + } + + if (!message || !message.rid) { + throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + return cmd.callback(command, params, message, triggerId); + }, + getPreviews(command: string, params: string, message: IMessage): SlashCommandPreviews | undefined { + const cmd = this.commands[command]; + if (typeof cmd?.previewer !== 'function') { + return; + } + + if (!message || !message.rid) { + throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + const previewInfo = cmd.previewer(command, params, message); + + if (!previewInfo?.items?.length) { + return; + } + + // A limit of ten results, to save time and bandwidth + if (previewInfo.items.length >= 10) { + previewInfo.items = previewInfo.items.slice(0, 10); + } + + return previewInfo; + }, + executePreview(command: string, params: string, message: IMessage, preview: SlashCommandPreviewItem, triggerId: string): void { + const cmd = this.commands[command]; + if (typeof cmd?.previewCallback !== 'function') { + return; + } + + if (!message || !message.rid) { + throw new Meteor.Error('invalid-command-usage', 'Executing a command requires at least a message with a room id.'); + } + + // { id, type, value } + if (!preview.id || !preview.type || !preview.value) { + throw new Meteor.Error('error-invalid-preview', 'Preview Item must have an id, type, and value.'); + } + + return cmd.previewCallback(command, params, message, preview, triggerId); + }, +}; + +Meteor.methods({ + slashCommand(command) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'slashCommand', + }); + } + + if (!command || !command.cmd || !slashCommands.commands[command.cmd]) { + throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', { + method: 'executeSlashCommandPreview', + }); + } + return slashCommands.run(command.cmd, command.params, command.msg, command.triggerId); + }, +}); 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/Message/Attachments/Attachment/Attachment.tsx b/apps/meteor/client/components/Message/Attachments/Attachment/Attachment.tsx index f7d5bcdce81d..d79ecac8e56c 100644 --- a/apps/meteor/client/components/Message/Attachments/Attachment/Attachment.tsx +++ b/apps/meteor/client/components/Message/Attachments/Attachment/Attachment.tsx @@ -1,11 +1,26 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import { useAttachmentDimensions } from '@rocket.chat/ui-contexts'; import React, { ComponentProps, FC } from 'react'; +const className = css` + white-space: normal; +`; + const Attachment: FC> = (props) => { const { width } = useAttachmentDimensions(); return ( - + ); }; diff --git a/apps/meteor/client/components/Message/Attachments/DefaultAttachment.tsx b/apps/meteor/client/components/Message/Attachments/DefaultAttachment.tsx index db2ae9426679..f526356fceed 100644 --- a/apps/meteor/client/components/Message/Attachments/DefaultAttachment.tsx +++ b/apps/meteor/client/components/Message/Attachments/DefaultAttachment.tsx @@ -1,5 +1,5 @@ import { isActionAttachment, MarkdownFields, MessageAttachmentDefault } from '@rocket.chat/core-typings'; -import React, { FC, ReactNode } from 'react'; +import React, { FC, ReactNode, ComponentProps } from 'react'; import MarkdownText from '../../MarkdownText'; import { ActionAttachment } from './ActionAttachtment'; @@ -11,7 +11,8 @@ const applyMarkdownIfRequires = ( list: MessageAttachmentDefault['mrkdwn_in'] = ['text', 'pretext'], key: MarkdownFields, text: string, -): ReactNode => (list?.includes(key) ? : text); + variant: ComponentProps['variant'] = 'inline', +): ReactNode => (list?.includes(key) ? : text); const DefaultAttachment: FC = (attachment) => { const [collapsed, collapse] = useCollapse(!!attachment.collapsed); @@ -58,7 +59,9 @@ const DefaultAttachment: FC = (attachment) => { )} {!collapsed && ( <> - {attachment.text && {applyMarkdownIfRequires(attachment.mrkdwn_in, 'text', attachment.text)}} + {attachment.text && ( + {applyMarkdownIfRequires(attachment.mrkdwn_in, 'text', attachment.text, 'document')} + )} {/* {attachment.fields && ({ ...rest, value: })) : attachment.fields} />} */} {attachment.fields && ( = ({ )} - + {attachments && ( diff --git a/apps/meteor/client/components/Message/MessageBodyRender/ASTMessageRender.tsx b/apps/meteor/client/components/Message/MessageBodyRender/ASTMessageRender.tsx index a7b1f11de907..fd5cecfec28c 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/ASTMessageRender.tsx +++ b/apps/meteor/client/components/Message/MessageBodyRender/ASTMessageRender.tsx @@ -66,6 +66,10 @@ const ASTMessageRender: FC = ({ return ; } + if (block.type === 'LINE_BREAK') { + return
      ; + } + return null; })} diff --git a/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx b/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx index 20c37a1aa66f..742abd1dca9d 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx +++ b/apps/meteor/client/components/Message/MessageBodyRender/OrderedList.tsx @@ -5,9 +5,9 @@ import Inline from './Inline'; const OrderedList: FC<{ value: ASTOrderedList['value'] }> = ({ value }) => (
        - {value.map((item, index) => ( -
      1. - + {value.map(({ value, number }, index) => ( +
      2. +
      3. ))}
      diff --git a/apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx b/apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx index 5b1c3ff7172d..5c0cd2c4011e 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx +++ b/apps/meteor/client/components/Message/MessageBodyRender/PlainText.tsx @@ -12,12 +12,6 @@ const PlainText: FC = ({ value: text }) => { const highlights = useMessageListHighlights(); const katex = useMessageListKatex(); - // TODO ENG DAY: - // Add style for empty paragraphs in fuselage - if (!text.trim()) { - return
      ; - } - if (highlights || katex) { return ; } diff --git a/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx b/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx index 137b2bf78768..e2a3076fa057 100644 --- a/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx +++ b/apps/meteor/client/components/Message/MessageBodyRender/UnorderedList.tsx @@ -5,8 +5,8 @@ import Inline from './Inline'; const UnorderedList: FC<{ value: ASTUnorderedList['value'] }> = ({ value }) => (
        - {value.map((item) => ( -
      • + {value.map((item, index) => ( +
      • ))} 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 => (