diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index 2a453ccdb4d2..b02b09a62a91 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -8,4 +8,4 @@ jobs: steps: - uses: ggazzo/gh-action-auto-label@beta-5 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RC_AUTOLABEL_TOKEN }} diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index e62ee8217486..58a9c122b3e4 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -18,7 +18,6 @@ env: MONGO_URL: mongodb://localhost:27017/rocketchat MONGO_OPLOG_URL: mongodb://mongo:27017/local TOOL_NODE_FLAGS: --max_old_space_size=4096 - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ secrets.TURBO_TEAM }} jobs: @@ -140,17 +139,17 @@ jobs: - name: TurboRepo local server uses: felixmosh/turborepo-gh-artifacts@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token: ${{ secrets.RC_TURBO_GH_TOKEN }} server-token: ${{ secrets.TURBO_SERVER_TOKEN }} - name: Lint - run: yarn lint --api="http://127.0.0.1:9080" + run: yarn lint --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: Translation check - run: yarn turbo run translation-check --api="http://127.0.0.1:9080" + run: yarn turbo run translation-check --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: TS typecheck - run: yarn turbo run typecheck --api="http://127.0.0.1:9080" + run: yarn turbo run typecheck --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: Reset Meteor if: startsWith(github.ref, 'refs/tags/') == 'true' || github.ref == 'refs/heads/develop' @@ -251,11 +250,11 @@ jobs: - name: TurboRepo local server uses: felixmosh/turborepo-gh-artifacts@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token: ${{ secrets.RC_TURBO_GH_TOKEN }} server-token: ${{ secrets.TURBO_SERVER_TOKEN }} - name: Unit Test - run: yarn testunit --api="http://127.0.0.1:9080" + run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: Restore build uses: actions/download-artifact@v2 @@ -410,14 +409,14 @@ jobs: - name: TurboRepo local server uses: felixmosh/turborepo-gh-artifacts@v1 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token: ${{ secrets.RC_TURBO_GH_TOKEN }} server-token: ${{ secrets.TURBO_SERVER_TOKEN }} - name: yarn install run: yarn - name: Unit Test - run: yarn testunit --api="http://127.0.0.1:9080" + run: yarn testunit --api="http://127.0.0.1:9080" --token="${{ secrets.TURBO_SERVER_TOKEN }}" --team='rc' - name: Restore build uses: actions/download-artifact@v2 @@ -681,7 +680,7 @@ jobs: docker logs presence --tail=50 cd ./apps/meteor - npm run test:playwright + IS_EE=true npm run test:playwright - name: Store playwright test trace uses: actions/upload-artifact@v2 diff --git a/.github/workflows/no-new-js-files.yml b/.github/workflows/no-new-js-files.yml index 6a7473643de3..77045946b52c 100644 --- a/.github/workflows/no-new-js-files.yml +++ b/.github/workflows/no-new-js-files.yml @@ -1,4 +1,4 @@ -name: "JS file preventer" +name: 'JS file preventer' on: pull_request: types: [opened, synchronize] diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml index c31a3aef54d2..7c3ec70779c2 100644 --- a/.github/workflows/pr-title-checker.yml +++ b/.github/workflows/pr-title-checker.yml @@ -1,4 +1,4 @@ -name: "PR Title Checker" +name: 'PR Title Checker' on: pull_request: types: [opened, edited] @@ -9,4 +9,4 @@ jobs: steps: - uses: thehanimo/pr-title-checker@v1.3.4 with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.RC_TITLE_CHECKER }} diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 484c2a077081..e77826fd14fc 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -20,7 +20,7 @@ import { } from '@rocket.chat/rest-typings'; import { Rooms, Subscriptions, Messages } from '../../../models/server'; -import { hasPermission, hasAllPermission } from '../../../authorization/server'; +import { hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; import { Team } from '../../../../server/sdk'; @@ -454,7 +454,7 @@ API.v1.addRoute( }, { async post() { - if (!hasAllPermission(this.userId, ['create-team', 'edit-room'])) { + if (!hasPermission(this.userId, 'create-team')) { return API.v1.unauthorized(); } @@ -464,6 +464,10 @@ API.v1.addRoute( return API.v1.failure('The parameter "channelId" or "channelName" is required'); } + if (!hasPermission(this.userId, 'edit-room', channelId)) { + return API.v1.unauthorized(); + } + const room = findChannelByIdOrName({ params: { roomId: channelId, diff --git a/apps/meteor/app/api/server/v1/e2e.js b/apps/meteor/app/api/server/v1/e2e.ts similarity index 75% rename from apps/meteor/app/api/server/v1/e2e.js rename to apps/meteor/app/api/server/v1/e2e.ts index f079e75b8119..13939b1caf0d 100644 --- a/apps/meteor/app/api/server/v1/e2e.js +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -1,16 +1,26 @@ +/* eslint-disable @typescript-eslint/camelcase */ import { Meteor } from 'meteor/meteor'; +import { + ise2eGetUsersOfRoomWithoutKeyParamsGET, + ise2eSetRoomKeyIDParamsPOST, + ise2eSetUserPublicAndPrivateKeysParamsPOST, + ise2eUpdateGroupKeyParamsPOST, +} from '@rocket.chat/rest-typings'; +import { IUser } from '@rocket.chat/core-typings'; import { API } from '../api'; API.v1.addRoute( 'e2e.fetchMyKeys', - { authRequired: true }, + { + authRequired: true, + }, { get() { - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('e2e.fetchMyKeys'); - }); + const result: { + public_key: string; + private_key: string; + } = Meteor.call('e2e.fetchMyKeys'); return API.v1.success(result); }, @@ -19,15 +29,17 @@ API.v1.addRoute( API.v1.addRoute( 'e2e.getUsersOfRoomWithoutKey', - { authRequired: true }, + { + authRequired: true, + validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, + }, { get() { const { rid } = this.queryParams; - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('e2e.getUsersOfRoomWithoutKey', rid); - }); + const result: { + users: IUser[]; + } = Meteor.call('e2e.getUsersOfRoomWithoutKey', rid); return API.v1.success(result); }, @@ -65,16 +77,18 @@ API.v1.addRoute( * schema: * $ref: '#/components/schemas/ApiFailureV1' */ + API.v1.addRoute( 'e2e.setRoomKeyID', - { authRequired: true }, + { + authRequired: true, + validateParams: ise2eSetRoomKeyIDParamsPOST, + }, { post() { const { rid, keyID } = this.bodyParams; - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('e2e.setRoomKeyID', rid, keyID)); - }); + Meteor.call('e2e.setRoomKeyID', rid, keyID); return API.v1.success(); }, @@ -114,18 +128,17 @@ API.v1.addRoute( */ API.v1.addRoute( 'e2e.setUserPublicAndPrivateKeys', - { authRequired: true }, + { + authRequired: true, + validateParams: ise2eSetUserPublicAndPrivateKeysParamsPOST, + }, { post() { - const { public_key, private_key } = this.bodyParams; - - Meteor.runAsUser(this.userId, () => { - API.v1.success( - Meteor.call('e2e.setUserPublicAndPrivateKeys', { - public_key, - private_key, - }), - ); + const { public_key, private_key } = Meteor.call('e2e.fetchMyKeys'); + + Meteor.call('e2e.setUserPublicAndPrivateKeys', { + public_key, + private_key, }); return API.v1.success(); @@ -168,14 +181,15 @@ API.v1.addRoute( */ API.v1.addRoute( 'e2e.updateGroupKey', - { authRequired: true }, + { + authRequired: true, + validateParams: ise2eUpdateGroupKeyParamsPOST, + }, { post() { const { uid, rid, key } = this.bodyParams; - Meteor.runAsUser(this.userId, () => { - API.v1.success(Meteor.call('e2e.updateGroupKey', rid, uid, key)); - }); + Meteor.call('e2e.updateGroupKey', rid, uid, key); return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/import.ts b/apps/meteor/app/api/server/v1/import.ts index adaf638c0df3..e3d6cc5ebecc 100644 --- a/apps/meteor/app/api/server/v1/import.ts +++ b/apps/meteor/app/api/server/v1/import.ts @@ -15,6 +15,7 @@ import { API } from '../api'; import { hasPermission } from '../../../authorization/server'; import { Imports } from '../../../models/server'; import { Importers } from '../../../importer/server'; +import { executeUploadImportFile } from '../../../importer/server/methods/uploadImportFile'; API.v1.addRoute( 'uploadImportFile', @@ -26,7 +27,7 @@ API.v1.addRoute( post() { const { binaryContent, contentType, fileName, importerKey } = this.bodyParams; - return API.v1.success(Meteor.call('uploadImportFile', binaryContent, contentType, fileName, importerKey)); + return API.v1.success(executeUploadImportFile(this.userId, binaryContent, contentType, fileName, importerKey)); }, }, ); diff --git a/apps/meteor/app/api/server/v1/voip/rooms.ts b/apps/meteor/app/api/server/v1/voip/rooms.ts index 1bc70ce928ba..1199f67044b1 100644 --- a/apps/meteor/app/api/server/v1/voip/rooms.ts +++ b/apps/meteor/app/api/server/v1/voip/rooms.ts @@ -1,8 +1,7 @@ -import { Match, check } from 'meteor/check'; import { Random } from 'meteor/random'; -import type { ILivechatAgent } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, IVoipRoom } from '@rocket.chat/core-typings'; +import { isVoipRoomProps, isVoipRoomsProps, isVoipRoomCloseProps } from '@rocket.chat/rest-typings/dist/v1/voip'; import { VoipRoom, LivechatVisitors, Users } from '@rocket.chat/models'; -import { isVoipRoomCloseProps } from '@rocket.chat/rest-typings/dist/v1/voip'; import { API } from '../../api'; import { LivechatVoip } from '../../../../../server/sdk'; @@ -25,6 +24,7 @@ const validateDateParams = (property: string, date: DateParam = {}): DateParam = const parseAndValidate = (property: string, date?: string): DateParam => { return validateDateParams(property, parseDateParams(date)); }; + /** * @openapi * /voip/server/api/v1/voip/room @@ -81,23 +81,38 @@ const parseAndValidate = (property: string, date?: string): DateParam => { * $ref: '#/components/schemas/ApiFailureV1' */ +const isRoomSearchProps = (props: any): props is { rid: string; token: string } => { + return 'rid' in props && 'token' in props; +}; + +const isRoomCreationProps = (props: any): props is { agentId: string; direction: IVoipRoom['direction'] } => { + return 'agentId' in props && 'direction' in props; +}; + API.v1.addRoute( 'voip/room', { authRequired: true, rateLimiterOptions: { numRequestsAllowed: 5, intervalTimeInMS: 60000 }, permissionsRequired: ['inbound-voip-calls'], + validateParams: isVoipRoomProps, }, { async get() { - const defaultCheckParams = { - token: String, - agentId: Match.Maybe(String), - rid: Match.Maybe(String), - }; - check(this.queryParams, defaultCheckParams); - - const { token, rid, agentId } = this.queryParams; + const { token } = this.queryParams; + let agentId: string | undefined = undefined; + let direction: IVoipRoom['direction'] = 'inbound'; + let rid: string | undefined = undefined; + + if (isRoomCreationProps(this.queryParams)) { + agentId = this.queryParams.agentId; + direction = this.queryParams.direction; + } + + if (isRoomSearchProps(this.queryParams)) { + rid = this.queryParams.rid; + } + const guest = await LivechatVisitors.getVisitorByToken(token, {}); if (!guest) { return API.v1.failure('invalid-token'); @@ -123,7 +138,11 @@ API.v1.addRoute( const agent = { agentId: _id, username }; const rid = Random.id(); - return API.v1.success(await LivechatVoip.getNewRoom(guest, agent, rid, { projection: API.v1.defaultFieldsToExclude })); + return API.v1.success( + await LivechatVoip.getNewRoom(guest, agent, rid, direction, { + projection: API.v1.defaultFieldsToExclude, + }), + ); } const room = await VoipRoom.findOneByIdAndVisitorToken(rid, token, { projection: API.v1.defaultFieldsToExclude }); @@ -137,20 +156,15 @@ API.v1.addRoute( API.v1.addRoute( 'voip/rooms', - { authRequired: true }, + { authRequired: true, validateParams: isVoipRoomsProps }, { async get() { const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); - const { agents, open, tags, queue, visitorId } = this.requestParams(); + const { agents, open, tags, queue, visitorId, direction, roomName } = this.requestParams(); const { createdAt: createdAtParam, closedAt: closedAtParam } = this.requestParams(); - check(agents, Match.Maybe([String])); - check(open, Match.Maybe(String)); - check(tags, Match.Maybe([String])); - check(queue, Match.Maybe(String)); - check(visitorId, Match.Maybe(String)); - // Reusing same L room permissions for simplicity const hasAdminAccess = hasPermission(this.userId, 'view-livechat-rooms'); const hasAgentAccess = hasPermission(this.userId, 'view-l-room') && agents?.includes(this.userId) && agents?.length === 1; @@ -170,6 +184,8 @@ API.v1.addRoute( visitorId, createdAt, closedAt, + direction, + roomName, options: { sort, offset, count, fields }, }), ); diff --git a/apps/meteor/app/emoji/client/emojiPicker.js b/apps/meteor/app/emoji/client/emojiPicker.js index 5995ed8bd746..671698280cc1 100644 --- a/apps/meteor/app/emoji/client/emojiPicker.js +++ b/apps/meteor/app/emoji/client/emojiPicker.js @@ -151,7 +151,7 @@ Template.emojiPicker.events({ 'click .add-custom'(event) { event.stopPropagation(); event.preventDefault(); - FlowRouter.go('/admin/emoji-custom'); + FlowRouter.go('/admin/emoji-custom/new'); EmojiPicker.close(); }, 'click .category-link'(event) { diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js index d8edfd156d9d..73b7b475f892 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; -import { Users, Rooms } from '../../../models/server'; +import { Users, Rooms, Settings } from '../../../models/server'; import { sendMessage } from '../../../lib'; class ErrorHandler { @@ -33,6 +33,7 @@ class ErrorHandler { process.on( 'uncaughtException', Meteor.bindEnvironment((error) => { + Settings.incrementValueById('Uncaught_Exceptions_Count'); if (!this.reporting) { return; } diff --git a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js index 6424c633bdbe..6facd718f0e4 100644 --- a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js +++ b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.js @@ -57,7 +57,7 @@ Meteor.methods({ importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1); + const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1).split('?')[0]; const date = new Date(); const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; const newFileName = `${dateStr}_${userId}_${oldFileName}`; diff --git a/apps/meteor/app/importer/server/methods/uploadImportFile.js b/apps/meteor/app/importer/server/methods/uploadImportFile.js index 115559664982..b23183210395 100644 --- a/apps/meteor/app/importer/server/methods/uploadImportFile.js +++ b/apps/meteor/app/importer/server/methods/uploadImportFile.js @@ -6,6 +6,38 @@ import { hasPermission } from '../../../authorization'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { Importers } from '..'; +export const executeUploadImportFile = (userId, binaryContent, contentType, fileName, importerKey) => { + const importer = Importers.get(importerKey); + if (!importer) { + throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, { + method: 'uploadImportFile', + }); + } + + importer.instance = new importer.importer(importer); // eslint-disable-line new-cap + + const date = new Date(); + const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; + const newFileName = `${dateStr}_${userId}_${fileName}`; + + // Store the file name and content type on the imports collection + importer.instance.startFileUpload(newFileName, contentType); + + // Save the file on the File Store + const file = Buffer.from(binaryContent, 'base64'); + const readStream = RocketChatFile.bufferToStream(file); + const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); + + writeStream.on( + 'end', + Meteor.bindEnvironment(() => { + importer.instance.updateProgress(ProgressStep.FILE_LOADED); + }), + ); + + readStream.pipe(writeStream); +}; + Meteor.methods({ uploadImportFile(binaryContent, contentType, fileName, importerKey) { const userId = Meteor.userId(); @@ -20,34 +52,6 @@ Meteor.methods({ }); } - const importer = Importers.get(importerKey); - if (!importer) { - throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, { - method: 'uploadImportFile', - }); - } - - importer.instance = new importer.importer(importer); // eslint-disable-line new-cap - - const date = new Date(); - const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; - const newFileName = `${dateStr}_${userId}_${fileName}`; - - // Store the file name and content type on the imports collection - importer.instance.startFileUpload(newFileName, contentType); - - // Save the file on the File Store - const file = Buffer.from(binaryContent, 'base64'); - const readStream = RocketChatFile.bufferToStream(file); - const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); - - writeStream.on( - 'end', - Meteor.bindEnvironment(() => { - importer.instance.updateProgress(ProgressStep.FILE_LOADED); - }), - ); - - readStream.pipe(writeStream); + executeUploadImportFile(userId, binaryContent, contentType, fileName, importerKey); }, }); diff --git a/apps/meteor/app/lib/server/lib/meteorFixes.js b/apps/meteor/app/lib/server/lib/meteorFixes.js index abc7a78855cb..1cbf11d6761d 100644 --- a/apps/meteor/app/lib/server/lib/meteorFixes.js +++ b/apps/meteor/app/lib/server/lib/meteorFixes.js @@ -1,6 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { MongoInternals } from 'meteor/mongo'; +import { Settings } from '../../../models/server'; + const timeoutQuery = parseInt(process.env.OBSERVERS_CHECK_TIMEOUT) || 2 * 60 * 1000; const interval = parseInt(process.env.OBSERVERS_CHECK_INTERVAL) || 60 * 1000; const debug = Boolean(process.env.OBSERVERS_CHECK_DEBUG); @@ -59,6 +61,7 @@ Meteor.setInterval(() => { */ process.on('unhandledRejection', (error) => { + Settings.incrementValueById('Uncaught_Exceptions_Count'); console.error('=== UnHandledPromiseRejection ==='); console.error(error); console.error('---------------------------------'); diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 112241a71f69..8ebc57282f7a 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -426,10 +426,23 @@ settingsRegistry.addGroup('Accounts', function () { i18nLabel: 'Sort_By', }); - this.add('Accounts_Default_User_Preferences_showMessageInMainThread', false, { - type: 'boolean', + this.add('Accounts_Default_User_Preferences_alsoSendThreadToChannel', 'default', { + type: 'select', + values: [ + { + key: 'default', + i18nLabel: 'Default', + }, + { + key: 'always', + i18nLabel: 'Always', + }, + { + key: 'never', + i18nLabel: 'Never', + }, + ], public: true, - i18nLabel: 'Show_Message_In_Main_Thread', }); this.add('Accounts_Default_User_Preferences_sidebarShowFavorites', true, { @@ -457,6 +470,7 @@ settingsRegistry.addGroup('Accounts', function () { public: true, i18nLabel: 'Enter_Behaviour', }); + this.add('Accounts_Default_User_Preferences_messageViewMode', 0, { type: 'select', values: [ @@ -1702,6 +1716,11 @@ settingsRegistry.addGroup('Logs', function () { }, }); + this.add('Uncaught_Exceptions_Count', 0, { + hidden: true, + type: 'int', + }); + this.section('Prometheus', function () { this.add('Prometheus_Enabled', false, { type: 'boolean', @@ -2911,6 +2930,10 @@ settingsRegistry.addGroup('Setup_Wizard', function () { this.add('Organization_Email', '', { type: 'string', }); + this.add('Triggered_Emails_Count', 0, { + type: 'int', + hidden: true, + }); }); this.section('Cloud_Info', function () { diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.js b/apps/meteor/app/livechat/server/api/v1/videoCall.js index efca6c1a06ff..60a26a7f4682 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.js +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.js @@ -4,7 +4,7 @@ import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { Messages, Rooms } from '../../../../models/server'; +import { Messages, Rooms, Settings } from '../../../../models'; import { settings as rcSettings } from '../../../../settings/server'; import { API } from '../../../../api/server'; import { findGuest, getRoom, settings } from '../lib/livechat'; @@ -107,6 +107,7 @@ API.v1.addRoute( let { callStatus } = room; if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { + Settings.incrementValueById('WebRTC_Calls_Count'); callStatus = 'ringing'; Promise.await(Rooms.setCallStatusAndCallStartTime(room._id, callStatus)); Promise.await( diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index ae4634fff0ae..05e75145fd4e 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -9,6 +9,7 @@ import { escapeHTML } from '@rocket.chat/string-helpers'; import type { ISetting } from '@rocket.chat/core-typings'; import { settings } from '../../settings/server'; +import { Settings as SettingsRaw } from '../../models/server'; import { replaceVariables } from './replaceVariables'; import { Apps } from '../../apps/server'; import { validateEmail } from '../../../lib/emailValidator'; @@ -169,6 +170,8 @@ export const sendNoWrap = ({ html = undefined; } + SettingsRaw.incrementValueById('Triggered_Emails_Count'); + const email = { to, from, replyTo, subject, html, text, headers }; const eventResult = Promise.await(Apps.triggerEvent('IPreEmailSent', { email })); diff --git a/apps/meteor/app/models/index.js b/apps/meteor/app/models/index.ts similarity index 100% rename from apps/meteor/app/models/index.js rename to apps/meteor/app/models/index.ts diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index 4ce7aeb74ec2..f21439fcd9fa 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -23,6 +23,16 @@ export class LivechatRooms extends Base { this.tryEnsureIndex({ t: 1, departmentId: 1, closedAt: 1 }, { partialFilterExpression: { closedAt: { $exists: true } } }); this.tryEnsureIndex({ source: 1 }, { sparse: true }); this.tryEnsureIndex({ departmentAncestors: 1 }, { sparse: true }); + this.tryEnsureIndex( + { 't': 1, 'open': 1, 'source.type': 1, 'v.status': 1 }, + { + partialFilterExpression: { + 't': { $eq: 'l' }, + 'open': { $eq: true }, + 'source.type': { $eq: 'widget' }, + }, + }, + ); } findLivechat(filter = {}, offset = 0, limit = 20) { diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index 7d6d4edbc772..66f8e8366f03 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -24,6 +24,8 @@ export class Rooms extends Base { this.tryEnsureIndex({ uids: 1 }, { sparse: true }); this.tryEnsureIndex({ createdOTR: 1 }, { sparse: true }); this.tryEnsureIndex({ encrypted: 1 }, { sparse: true }); // used on statistics + this.tryEnsureIndex({ broadcast: 1 }, { sparse: true }); // used on statistics + this.tryEnsureIndex({ 'streamingOptions.type': 1 }, { sparse: true }); // used on statistics this.tryEnsureIndex( { diff --git a/apps/meteor/app/oembed/client/index.js b/apps/meteor/app/oembed/client/index.ts similarity index 100% rename from apps/meteor/app/oembed/client/index.js rename to apps/meteor/app/oembed/client/index.ts diff --git a/apps/meteor/app/oembed/server/index.js b/apps/meteor/app/oembed/server/index.ts similarity index 100% rename from apps/meteor/app/oembed/server/index.js rename to apps/meteor/app/oembed/server/index.ts diff --git a/apps/meteor/app/oembed/server/jumpToMessage.js b/apps/meteor/app/oembed/server/jumpToMessage.ts similarity index 67% rename from apps/meteor/app/oembed/server/jumpToMessage.js rename to apps/meteor/app/oembed/server/jumpToMessage.ts index 8bded0996ed9..29168b70ccb3 100644 --- a/apps/meteor/app/oembed/server/jumpToMessage.js +++ b/apps/meteor/app/oembed/server/jumpToMessage.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/camelcase */ import URL from 'url'; import QueryString from 'querystring'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; +import { ITranslatedMessage, MessageAttachment, isQuoteAttachment } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users } from '../../models/server'; import { settings } from '../../settings/server'; @@ -10,14 +12,25 @@ import { callbacks } from '../../../lib/callbacks'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; import { canAccessRoom } from '../../authorization/server/functions/canAccessRoom'; -const recursiveRemove = (message, deep = 1) => { - if (message) { - if ('attachments' in message && message.attachments !== null && deep < settings.get('Message_QuoteChainLimit')) { - message.attachments.map((msg) => recursiveRemove(msg, deep + 1)); +const recursiveRemove = (attachments: MessageAttachment, deep = 1): MessageAttachment => { + if (attachments && isQuoteAttachment(attachments)) { + if (deep < settings.get('Message_QuoteChainLimit')) { + attachments.attachments?.map((msg) => recursiveRemove(msg, deep + 1)); } else { - delete message.attachments; + delete attachments.attachments; } } + + return attachments; +}; + +const validateAttachmentDeepness = (message: ITranslatedMessage): ITranslatedMessage => { + if (!message || !message.attachments) { + return message; + } + + message.attachments = message.attachments?.map((attachment) => recursiveRemove(attachment)); + return message; }; @@ -50,7 +63,7 @@ callbacks.add( return; } - const jumpToMessage = recursiveRemove(Messages.findOneById(msgId)); + const jumpToMessage = validateAttachmentDeepness(Messages.findOneById(msgId)); if (!jumpToMessage) { return; } @@ -65,7 +78,8 @@ callbacks.add( } msg.attachments = msg.attachments || []; - const index = msg.attachments.findIndex((a) => a.message_link === item.url); + // Only QuoteAttachments have "message_link" property + const index = msg.attachments.findIndex((a) => isQuoteAttachment(a) && a.message_link === item.url); if (index > -1) { msg.attachments.splice(index, 1); } @@ -76,6 +90,7 @@ callbacks.add( author_name: jumpToMessage.alias || jumpToMessage.u.username, author_icon: getUserAvatarURL(jumpToMessage.u.username), message_link: item.url, + // @ts-expect-error attachments: jumpToMessage.attachments || [], ts: jumpToMessage.ts, }); diff --git a/apps/meteor/app/oembed/server/providers.js b/apps/meteor/app/oembed/server/providers.ts similarity index 74% rename from apps/meteor/app/oembed/server/providers.js rename to apps/meteor/app/oembed/server/providers.ts index 1f89975e372e..fae14bfa0a9e 100644 --- a/apps/meteor/app/oembed/server/providers.js +++ b/apps/meteor/app/oembed/server/providers.ts @@ -3,31 +3,38 @@ import QueryString from 'querystring'; import { camelCase } from 'change-case'; import _ from 'underscore'; +import { OEmbedMeta, OEmbedUrlContent, ParsedUrl, OEmbedProvider } from '@rocket.chat/core-typings'; import { callbacks } from '../../../lib/callbacks'; import { SystemLogger } from '../../../server/lib/logger/system'; +type OEmbedExecutor = { + providers: Providers; +}; + class Providers { + private providers: OEmbedProvider[]; + constructor() { this.providers = []; } - static getConsumerUrl(provider, url) { - const urlObj = URL.parse(provider.endPoint, true); - urlObj.query.url = url; - delete urlObj.search; + static getConsumerUrl(provider: OEmbedProvider, url: string): string { + const urlObj = new URL.URL(provider.endPoint); + urlObj.searchParams.set('url', url); + return URL.format(urlObj); } - registerProvider(provider) { + registerProvider(provider: OEmbedProvider): number { return this.providers.push(provider); } - getProviders() { + getProviders(): OEmbedProvider[] { return this.providers; } - getProviderForUrl(url) { + getProviderForUrl(url: string): OEmbedProvider | undefined { return _.find(this.providers, function (provider) { const candidate = _.find(provider.urls, function (re) { return re.test(url); @@ -83,9 +90,9 @@ providers.registerProvider({ endPoint: 'https://open.spotify.com/oembed', }); -export const oembed = {}; - -oembed.providers = providers; +export const oembed: OEmbedExecutor = { + providers, +}; callbacks.add( 'oembed:beforeGetUrlContent', @@ -94,13 +101,16 @@ callbacks.add( const url = URL.format(data.parsedUrl); const provider = providers.getProviderForUrl(url); if (provider != null) { - let consumerUrl = Providers.getConsumerUrl(provider, url); - consumerUrl = URL.parse(consumerUrl, true); - _.extend(data.parsedUrl, consumerUrl); - data.urlObj.port = consumerUrl.port; - data.urlObj.hostname = consumerUrl.hostname; - data.urlObj.pathname = consumerUrl.pathname; - data.urlObj.query = consumerUrl.query; + const consumerUrl = Providers.getConsumerUrl(provider, url); + + const parsedConsumerUrl = URL.parse(consumerUrl, true); + _.extend(data.parsedUrl, parsedConsumerUrl); + + data.urlObj.port = parsedConsumerUrl.port; + data.urlObj.hostname = parsedConsumerUrl.hostname; + data.urlObj.pathname = parsedConsumerUrl.pathname; + data.urlObj.query = parsedConsumerUrl.query; + delete data.urlObj.search; delete data.urlObj.host; } @@ -111,7 +121,19 @@ callbacks.add( 'oembed-providers-before', ); -const cleanupOembed = (data) => { +const cleanupOembed = (data: { + url: string; + meta: OEmbedMeta; + headers: { [k: string]: string }; + parsedUrl: ParsedUrl; + content: OEmbedUrlContent; +}): { + url: string; + meta: Omit; + headers: { [k: string]: string }; + parsedUrl: ParsedUrl; + content: OEmbedUrlContent; +} => { if (!data?.meta) { return data; } diff --git a/apps/meteor/app/oembed/server/server.js b/apps/meteor/app/oembed/server/server.ts similarity index 52% rename from apps/meteor/app/oembed/server/server.js rename to apps/meteor/app/oembed/server/server.ts index 24bca83f7976..bbc577445441 100644 --- a/apps/meteor/app/oembed/server/server.js +++ b/apps/meteor/app/oembed/server/server.ts @@ -7,23 +7,31 @@ import iconv from 'iconv-lite'; import ipRangeCheck from 'ip-range-check'; import he from 'he'; import jschardet from 'jschardet'; +import { + OEmbedUrlContentResult, + OEmbedUrlWithMetadata, + IMessage, + MessageAttachment, + isOEmbedUrlContentResult, + isOEmbedUrlWithMetadata, + OEmbedMeta, +} from '@rocket.chat/core-typings'; import { OEmbedCache } from '@rocket.chat/models'; +import { Logger } from '../../logger/server'; import { Messages } from '../../models/server'; import { callbacks } from '../../../lib/callbacks'; import { settings } from '../../settings/server'; import { isURL } from '../../../lib/utils/isURL'; -import { SystemLogger } from '../../../server/lib/logger/system'; import { Info } from '../../utils/server'; import { fetch } from '../../../server/lib/http/fetch'; -const OEmbed = {}; - +const log = new Logger('OEmbed'); // Detect encoding // Priority: // Detected == HTTP Header > Detected == HTML meta > HTTP Header > HTML meta > Detected > Default (utf-8) // See also: https://www.w3.org/International/questions/qa-html-encoding-declarations.en#quickanswer -const getCharset = function (contentType, body) { +const getCharset = function (contentType: string, body: Buffer): string { let detectedCharset; let httpHeaderCharset; let htmlMetaCharset; @@ -57,34 +65,39 @@ const getCharset = function (contentType, body) { return result || 'utf-8'; }; -const toUtf8 = function (contentType, body) { +const toUtf8 = function (contentType: string, body: Buffer): string { return iconv.decode(body, getCharset(contentType, body)); }; -const getUrlContent = async function (urlObj, redirectCount = 5) { - if (_.isString(urlObj)) { - urlObj = URL.parse(urlObj); +const getUrlContent = async function (urlObjStr: string | URL.UrlWithStringQuery, redirectCount = 5): Promise { + let urlObj: URL.UrlWithStringQuery; + if (typeof urlObjStr === 'string') { + urlObj = URL.parse(urlObjStr); + } else { + urlObj = urlObjStr; } - const portsProtocol = { - 80: 'http:', - 8080: 'http:', - 443: 'https:', - }; + const portsProtocol = new Map( + Object.entries({ + 80: 'http:', + 8080: 'http:', + 443: 'https:', + }), + ); const parsedUrl = _.pick(urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query', 'search', 'hostname']); - const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; - if (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts)) { + const ignoredHosts = settings.get('API_EmbedIgnoredHosts').replace(/\s/g, '').split(',') || []; + if (parsedUrl.hostname && (ignoredHosts.includes(parsedUrl.hostname) || ipRangeCheck(parsedUrl.hostname, ignoredHosts))) { throw new Error('invalid host'); } - const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; + const safePorts = settings.get('API_EmbedSafePorts').replace(/\s/g, '').split(',') || []; if (safePorts.length > 0 && parsedUrl.port && !safePorts.includes(parsedUrl.port)) { throw new Error('invalid/unsafe port'); } - if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol[port] === parsedUrl.protocol)) { + if (safePorts.length > 0 && !parsedUrl.port && !safePorts.some((port) => portsProtocol.get(port) === parsedUrl.protocol)) { throw new Error('invalid/unsafe port'); } @@ -92,14 +105,17 @@ const getUrlContent = async function (urlObj, redirectCount = 5) { urlObj, parsedUrl, }); + + /* This prop is neither passed or returned by the callback, so I'll just comment it for now if (data.attachments != null) { return data; - } + } */ const url = URL.format(data.urlObj); const sizeLimit = 250000; + log.debug(`Fetching ${url} following redirects ${redirectCount} times`); const response = await fetch( url, { @@ -116,30 +132,37 @@ const getUrlContent = async function (urlObj, redirectCount = 5) { let totalSize = 0; const chunks = []; + // @ts-expect-error from https://github.com/microsoft/TypeScript/issues/39051 for await (const chunk of response.body) { totalSize += chunk.length; chunks.push(chunk); if (totalSize > sizeLimit) { - SystemLogger.info({ msg: 'OEmbed request size exceeded', url }); + log.warn({ msg: 'OEmbed request size exceeded', url }); break; } } + log.debug('Obtained response from server with length of', totalSize); const buffer = Buffer.concat(chunks); - return { + // @ts-expect-error - fetch types are kinda weird headers: Object.fromEntries(response.headers), - body: toUtf8(response.headers.get('content-type'), buffer), + body: toUtf8(response.headers.get('content-type') || 'text/plain', buffer), parsedUrl, statusCode: response.status, }; }; -OEmbed.getUrlMeta = function (url, withFragment) { +const getUrlMeta = async function ( + url: string, + withFragment?: boolean, +): Promise { + log.debug('Obtaining metadata for URL', url); const urlObj = URL.parse(url); if (withFragment != null) { - const queryStringObj = querystring.parse(urlObj.query); + const queryStringObj = querystring.parse(urlObj.query || ''); + // eslint-disable-next-line @typescript-eslint/camelcase queryStringObj._escaped_fragment_ = ''; urlObj.query = querystring.stringify(queryStringObj); let path = urlObj.pathname; @@ -149,7 +172,13 @@ OEmbed.getUrlMeta = function (url, withFragment) { } urlObj.path = path; } - const content = Promise.await(getUrlContent(urlObj, 5)); + log.debug('Fetching url content', urlObj.path); + let content: OEmbedUrlContentResult | undefined; + try { + content = await getUrlContent(urlObj, 5); + } catch (e) { + log.error('Error fetching url content', e); + } if (!content) { return; @@ -159,37 +188,37 @@ OEmbed.getUrlMeta = function (url, withFragment) { return content; } - let metas = undefined; - if (content && content.body) { - metas = {}; - const escapeMeta = (name, value) => { + log.debug('Parsing metadata for URL', url); + const metas: { [k: string]: string } = {}; + + if (content?.body) { + const escapeMeta = (name: string, value: string): string => { metas[name] = metas[name] || he.unescape(value); return metas[name]; }; - content.body.replace(/]*>([^<]*)<\/title>/gim, function (meta, title) { + content.body.replace(/]*>([^<]*)<\/title>/gim, function (_meta, title) { return escapeMeta('pageTitle', title); }); - content.body.replace(/]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gim, function (meta, name, value) { + content.body.replace(/]*(?:name|property)=[']([^']*)['][^>]*\scontent=[']([^']*)['][^>]*>/gim, function (_meta, name, value) { return escapeMeta(camelCase(name), value); }); - content.body.replace(/]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gim, function (meta, name, value) { + content.body.replace(/]*(?:name|property)=["]([^"]*)["][^>]*\scontent=["]([^"]*)["][^>]*>/gim, function (_meta, name, value) { return escapeMeta(camelCase(name), value); }); - content.body.replace(/]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gim, function (meta, value, name) { + content.body.replace(/]*\scontent=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gim, function (_meta, value, name) { return escapeMeta(camelCase(name), value); }); - content.body.replace(/]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gim, function (meta, value, name) { + content.body.replace(/]*\scontent=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gim, function (_meta, value, name) { return escapeMeta(camelCase(name), value); }); if (metas.fragment === '!' && withFragment == null) { - return OEmbed.getUrlMeta(url, true); + return getUrlMeta(url, true); } delete metas.oembedHtml; } - let headers = undefined; + const headers: { [k: string]: string } = {}; if (content?.headers) { - headers = {}; const headerObj = content.headers; Object.keys(headerObj).forEach((header) => { headers[camelCase(header)] = headerObj[header]; @@ -207,40 +236,49 @@ OEmbed.getUrlMeta = function (url, withFragment) { }); }; -OEmbed.getUrlMetaWithCache = async function (url, withFragment) { +const getUrlMetaWithCache = async function ( + url: string, + withFragment?: boolean, +): Promise { + log.debug('Getting oembed metadata for', url); const cache = await OEmbedCache.findOneById(url); if (cache != null) { + log.debug('Found oembed metadata in cache for', url); return cache.data; } - const data = OEmbed.getUrlMeta(url, withFragment); + const data = await getUrlMeta(url, withFragment); if (data != null) { try { + log.debug('Saving oembed metadata in cache for', url); await OEmbedCache.createWithIdAndData(url, data); } catch (_error) { - SystemLogger.error({ msg: 'OEmbed duplicated record', url }); + log.error({ msg: 'OEmbed duplicated record', url }); } return data; } }; -const getRelevantHeaders = function (headersObj) { - const headers = {}; - Object.keys(headersObj).forEach((key) => { - const value = headersObj[key]; - const lowerCaseKey = key.toLowerCase(); - if ((lowerCaseKey === 'contenttype' || lowerCaseKey === 'contentlength') && value && value.trim() !== '') { - headers[key] = value; - } - }); +const hasOnlyContentLength = (obj: any): obj is { contentLength: string } => 'contentLength' in obj && Object.keys(obj).length === 1; +const hasOnlyContentType = (obj: any): obj is { contentType: string } => 'contentType' in obj && Object.keys(obj).length === 1; +const hasContentLengthAndContentType = (obj: any): obj is { contentLength: string; contentType: string } => + 'contentLength' in obj && 'contentType' in obj && Object.keys(obj).length === 2; + +const getRelevantHeaders = function (headersObj: { + [key: string]: string; +}): { contentLength: string } | { contentType: string } | { contentLength: string; contentType: string } | void { + const headers = { + ...(headersObj.contentLength && { contentLength: headersObj.contentLength }), + ...(headersObj.contentType && { contentType: headersObj.contentType }), + }; - if (Object.keys(headers).length > 0) { + if (hasOnlyContentLength(headers) || hasOnlyContentType(headers) || hasContentLengthAndContentType(headers)) { return headers; } }; -const getRelevantMetaTags = function (metaObj) { - const tags = {}; +const getRelevantMetaTags = function (metaObj: OEmbedMeta): Record | void { + const tags: Record = {}; Object.keys(metaObj).forEach((key) => { const value = metaObj[key]; if (/^(og|fb|twitter|oembed|msapplication).+|description|title|pageTitle$/.test(key.toLowerCase()) && value && value.trim() !== '') { @@ -253,33 +291,41 @@ const getRelevantMetaTags = function (metaObj) { } }; -const insertMaxWidthInOembedHtml = (oembedHtml) => oembedHtml?.replace('iframe', 'iframe style="max-width: 100%;width:400px;height:225px"'); +const insertMaxWidthInOembedHtml = (oembedHtml?: string): string | undefined => + oembedHtml?.replace('iframe', 'iframe style="max-width: 100%;width:400px;height:225px"'); -OEmbed.rocketUrlParser = async function (message) { +const rocketUrlParser = async function (message: IMessage): Promise { + log.debug('Parsing message URLs'); if (Array.isArray(message.urls)) { - const attachments = []; + log.debug('URLs found', message.urls.length); + const attachments: MessageAttachment[] = []; + let changed = false; for await (const item of message.urls) { if (item.ignoreParse === true) { - return; + log.debug('URL ignored', item.url); + break; } if (!isURL(item.url)) { - return; + break; } - const data = await OEmbed.getUrlMetaWithCache(item.url); + const data = await getUrlMetaWithCache(item.url); if (data != null) { - if (data.attachments) { + if (isOEmbedUrlContentResult(data) && data.attachments) { attachments.push(...data.attachments); - return; + break; } - if (data.meta != null) { - item.meta = getRelevantMetaTags(data.meta); + if (isOEmbedUrlWithMetadata(data) && data.meta != null) { + item.meta = getRelevantMetaTags(data.meta) || {}; if (item.meta && item.meta.oembedHtml) { - item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml); + item.meta.oembedHtml = insertMaxWidthInOembedHtml(item.meta.oembedHtml) || ''; } } if (data.headers != null) { - item.headers = getRelevantHeaders(data.headers); + const headers = getRelevantHeaders(data.headers); + if (headers) { + item.headers = headers; + } } item.parsedUrl = data.parsedUrl; changed = true; @@ -295,6 +341,16 @@ OEmbed.rocketUrlParser = async function (message) { return message; }; +const OEmbed: { + getUrlMeta: (url: string, withFragment?: boolean) => Promise; + getUrlMetaWithCache: (url: string, withFragment?: boolean) => Promise; + rocketUrlParser: (message: IMessage) => Promise; +} = { + rocketUrlParser, + getUrlMetaWithCache, + getUrlMeta, +}; + settings.watch('API_Embed', function (value) { if (value) { return callbacks.add( diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 3efc88d46d0d..61efbfc4ceff 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -19,6 +19,7 @@ import { EmailInbox, LivechatBusinessHours, Messages as MessagesRaw, + Roles as RolesRaw, InstanceStatus, } from '@rocket.chat/models'; @@ -467,6 +468,9 @@ export const statistics = { statistics.slashCommandsJitsi = settings.get('Jitsi_Start_SlashCommands_Count'); statistics.totalOTRRooms = Rooms.findByCreatedOTR().count(); statistics.totalOTR = settings.get('OTR_Count'); + statistics.totalBroadcastRooms = await RoomsRaw.findByBroadcast().count(); + statistics.totalRoomsWithActiveLivestream = await RoomsRaw.findByActiveLivestream().count(); + statistics.totalTriggeredEmails = settings.get('Triggered_Emails_Count'); statistics.totalRoomsWithStarred = await MessagesRaw.countRoomsWithStarredMessages({ readPreference }); statistics.totalRoomsWithPinned = await MessagesRaw.countRoomsWithPinnedMessages({ readPreference }); statistics.totalUserTOTP = await UsersRaw.findActiveUsersTOTPEnable({ readPreference }).count(); @@ -478,10 +482,32 @@ export const statistics = { 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'); + statistics.totalSubscriptionRoles = await RolesRaw.findByScope('Subscriptions').count(); + statistics.totalUserRoles = await RolesRaw.findByScope('Users').count(); + statistics.totalWebRTCCalls = settings.get('WebRTC_Calls_Count'); + statistics.matrixBridgeEnabled = settings.get('Federation_Matrix_enabled'); + statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); + + const defaultHomeTitle = Settings.findOneById('Layout_Home_Title').packageValue; + statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; + + const defaultHomeBody = Settings.findOneById('Layout_Home_Body').packageValue; + statistics.homeBodyChanged = settings.get('Layout_Home_Body') !== defaultHomeBody; + + const defaultCustomCSS = Settings.findOneById('theme-custom-css').packageValue; + statistics.customCSSChanged = settings.get('theme-custom-css') !== defaultCustomCSS; + + const defaultOnLogoutCustomScript = Settings.findOneById('Custom_Script_On_Logout').packageValue; + statistics.onLogoutCustomScriptChanged = settings.get('Custom_Script_On_Logout') !== defaultOnLogoutCustomScript; + + const defaultLoggedOutCustomScript = Settings.findOneById('Custom_Script_Logged_Out').packageValue; + statistics.loggedOutCustomScriptChanged = settings.get('Custom_Script_Logged_Out') !== defaultLoggedOutCustomScript; + + const defaultLoggedInCustomScript = Settings.findOneById('Custom_Script_Logged_In').packageValue; + statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; await Promise.all(statsPms).catch(log); diff --git a/apps/meteor/app/threads/client/flextab/thread.js b/apps/meteor/app/threads/client/flextab/thread.js index da1cd730642b..6c71447980be 100644 --- a/apps/meteor/app/threads/client/flextab/thread.js +++ b/apps/meteor/app/threads/client/flextab/thread.js @@ -77,6 +77,8 @@ Template.thread.helpers({ } = Template.currentData(); const showFormattingTips = settings.get('Message_ShowFormattingTips'); + const alsoSendPreferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); + return { showFormattingTips, tshow: instance.state.get('sendToChannel'), @@ -85,7 +87,9 @@ Template.thread.helpers({ tmid, onSend: (...args) => { instance.sendToBottom(); - instance.state.set('sendToChannel', false); + if (alsoSendPreferenceState === 'default') { + instance.state.set('sendToChannel', false); + } return instance.chatMessages && instance.chatMessages.send.apply(instance.chatMessages, args); }, onKeyUp: (...args) => instance.chatMessages && instance.chatMessages.keyup.apply(instance.chatMessages, args), @@ -243,8 +247,22 @@ Template.thread.onRendered(function () { Template.thread.onCreated(async function () { this.Threads = new Mongo.Collection(null); + const preferenceState = getUserPreference(Meteor.userId(), 'alsoSendThreadToChannel'); + + let sendToChannel; + switch (preferenceState) { + case 'always': + sendToChannel = true; + break; + case 'never': + sendToChannel = false; + break; + default: + sendToChannel = !this.data.mainMessage.tcount; + } + this.state = new ReactiveDict({ - sendToChannel: !this.data.mainMessage.tcount, + sendToChannel, }); this.loadMore = async () => { diff --git a/apps/meteor/app/ui-message/client/message.html b/apps/meteor/app/ui-message/client/message.html index 4837a2bb0df5..fb29dbfbd5dd 100644 --- a/apps/meteor/app/ui-message/client/message.html +++ b/apps/meteor/app/ui-message/client/message.html @@ -1,5 +1,5 @@