From 2ec1b9dfc25940f29ddb49711aeead691f5265ce Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 27 Jun 2019 23:17:37 -0300 Subject: [PATCH 01/87] Bump version to 1.3.0-develop --- .docker/Dockerfile.rhel | 2 +- .travis/snap.sh | 2 +- app/utils/rocketchat.info | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index b761aa183ea3..5aea58529f1c 100644 --- a/.docker/Dockerfile.rhel +++ b/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/rhscl/nodejs-8-rhel7 -ENV RC_VERSION 1.2.0 +ENV RC_VERSION 1.3.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.travis/snap.sh b/.travis/snap.sh index c08229cd2ced..ce2b416d6b99 100755 --- a/.travis/snap.sh +++ b/.travis/snap.sh @@ -17,7 +17,7 @@ elif [[ $TRAVIS_TAG ]]; then RC_VERSION=$TRAVIS_TAG else CHANNEL=edge - RC_VERSION=1.2.0 + RC_VERSION=1.3.0-develop fi echo "Preparing to trigger a snap release for $CHANNEL channel" diff --git a/app/utils/rocketchat.info b/app/utils/rocketchat.info index 6664c2c1265c..a708fdba7121 100644 --- a/app/utils/rocketchat.info +++ b/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "1.2.0" + "version": "1.3.0-develop" } diff --git a/package.json b/package.json index 1894c11eb6e6..03276992b957 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Rocket.Chat", "description": "The Ultimate Open Source WebChat Platform", - "version": "1.2.0", + "version": "1.3.0-develop", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" From 8c3e8bb4530d77a0ec27c902a497e1b2a7817e1a Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 28 Jun 2019 18:14:57 -0300 Subject: [PATCH 02/87] [FIX] Not showing local app on App Details (#14894) --- app/apps/client/admin/appManage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/apps/client/admin/appManage.js b/app/apps/client/admin/appManage.js index 563a42d1d2cf..1b4e8261f8b3 100644 --- a/app/apps/client/admin/appManage.js +++ b/app/apps/client/admin/appManage.js @@ -29,7 +29,7 @@ function getApps(instance) { return Promise.resolve({ app: undefined }); }) .then((remote) => { - if (!remote.app.bundledIn || remote.app.bundledIn.length === 0) { + if (!remote.app || !remote.app.bundledIn || remote.app.bundledIn.length === 0) { return remote; } From 0bb9a522bbfda4aeb1070b87f741069c425957b1 Mon Sep 17 00:00:00 2001 From: Aaron Ogle Date: Mon, 1 Jul 2019 17:05:28 -0500 Subject: [PATCH 03/87] get cloud generated public key for marketplace licenses (#14851) --- app/cloud/server/functions/connectWorkspace.js | 1 + app/cloud/server/functions/getWorkspaceKey.js | 18 ++++++++++++++++++ app/cloud/server/functions/syncWorkspace.js | 10 +++++++++- .../server/functions/unregisterWorkspace.js | 1 + app/cloud/server/index.js | 3 ++- app/lib/server/startup/settings.js | 11 +++++++++++ 6 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 app/cloud/server/functions/getWorkspaceKey.js diff --git a/app/cloud/server/functions/connectWorkspace.js b/app/cloud/server/functions/connectWorkspace.js index 5ec79dbdb7e5..7f62a8b8b9c2 100644 --- a/app/cloud/server/functions/connectWorkspace.js +++ b/app/cloud/server/functions/connectWorkspace.js @@ -51,6 +51,7 @@ export function connectWorkspace(token) { Settings.updateValueById('Cloud_Workspace_Client_Id', data.client_id); Settings.updateValueById('Cloud_Workspace_Client_Secret', data.client_secret); Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', data.client_secret_expires_at); + Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', data.registration_client_uri); // Now that we have the client id and secret, let's get the access token diff --git a/app/cloud/server/functions/getWorkspaceKey.js b/app/cloud/server/functions/getWorkspaceKey.js new file mode 100644 index 000000000000..912163ee02e5 --- /dev/null +++ b/app/cloud/server/functions/getWorkspaceKey.js @@ -0,0 +1,18 @@ +import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; +import { settings } from '../../../settings'; + +export function getWorkspaceKey() { + const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus(); + + if (!connectToCloud || !workspaceRegistered) { + return false; + } + + const publicKey = settings.get('Cloud_Workspace_PublicKey'); + + if (!publicKey) { + return false; + } + + return publicKey; +} diff --git a/app/cloud/server/functions/syncWorkspace.js b/app/cloud/server/functions/syncWorkspace.js index 93f1cddbc134..57f9645f8357 100644 --- a/app/cloud/server/functions/syncWorkspace.js +++ b/app/cloud/server/functions/syncWorkspace.js @@ -5,6 +5,7 @@ import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { getWorkspaceLicense } from './getWorkspaceLicense'; import { statistics } from '../../../statistics'; +import { Settings } from '../../../models'; import { settings } from '../../../settings'; export function syncWorkspace(reconnectCheck = false) { @@ -31,6 +32,7 @@ export function syncWorkspace(reconnectCheck = false) { const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); + let result; try { const headers = {}; const token = getWorkspaceAccessToken(true); @@ -41,7 +43,7 @@ export function syncWorkspace(reconnectCheck = false) { return false; } - HTTP.post(`${ workspaceUrl }/client`, { + result = HTTP.post(`${ workspaceUrl }/client`, { data: info, headers, }); @@ -57,5 +59,11 @@ export function syncWorkspace(reconnectCheck = false) { return false; } + const { data } = result; + + if (data.publicKey) { + Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); + } + return true; } diff --git a/app/cloud/server/functions/unregisterWorkspace.js b/app/cloud/server/functions/unregisterWorkspace.js index a1102b1505d0..761932b0a34f 100644 --- a/app/cloud/server/functions/unregisterWorkspace.js +++ b/app/cloud/server/functions/unregisterWorkspace.js @@ -13,6 +13,7 @@ export function unregisterWorkspace() { Settings.updateValueById('Cloud_Workspace_Client_Id', null); Settings.updateValueById('Cloud_Workspace_Client_Secret', null); Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', null); + Settings.updateValueById('Cloud_Workspace_PublicKey', null); Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', null); return true; diff --git a/app/cloud/server/index.js b/app/cloud/server/index.js index 4a2e16941c00..3045794d7f5b 100644 --- a/app/cloud/server/index.js +++ b/app/cloud/server/index.js @@ -2,6 +2,7 @@ import './methods'; import { getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken'; import { getWorkspaceLicense } from './functions/getWorkspaceLicense'; import { getUserCloudAccessToken } from './functions/getUserCloudAccessToken'; +import { getWorkspaceKey } from './functions/getWorkspaceKey'; import { Permissions } from '../../models'; if (Permissions) { @@ -11,4 +12,4 @@ if (Permissions) { // Ensure the client/workspace access token is valid getWorkspaceAccessToken(); -export { getWorkspaceAccessToken, getWorkspaceLicense, getUserCloudAccessToken }; +export { getWorkspaceAccessToken, getWorkspaceLicense, getWorkspaceKey, getUserCloudAccessToken }; diff --git a/app/lib/server/startup/settings.js b/app/lib/server/startup/settings.js index 9d83ead14338..adf0e1893a22 100644 --- a/app/lib/server/startup/settings.js +++ b/app/lib/server/startup/settings.js @@ -2664,6 +2664,17 @@ settings.addGroup('Setup_Wizard', function() { secret: true, }); + this.add('Cloud_Workspace_PublicKey', '', { + type: 'string', + hidden: true, + readonly: true, + enableQuery: { + _id: 'Register_Server', + value: true, + }, + secret: true, + }); + this.add('Cloud_Workspace_License', '', { type: 'string', hidden: true, From a3fb62f94a98f49a53e87f00e9eead15b9875f0a Mon Sep 17 00:00:00 2001 From: Philipp Kolmann Date: Tue, 2 Jul 2019 16:33:59 +0200 Subject: [PATCH 04/87] [FIX] SAML login by giving displayName priority over userName for fullName (#14880) --- app/meteor-accounts-saml/server/saml_server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/meteor-accounts-saml/server/saml_server.js b/app/meteor-accounts-saml/server/saml_server.js index eedb919bd69e..65386015cac6 100644 --- a/app/meteor-accounts-saml/server/saml_server.js +++ b/app/meteor-accounts-saml/server/saml_server.js @@ -116,7 +116,7 @@ Accounts.registerLoginHandler(function(loginRequest) { const emailRegex = new RegExp(emailList.map((email) => `^${ RegExp.escape(email) }$`).join('|'), 'i'); const eduPersonPrincipalName = loginResult.profile.eppn; - const fullName = loginResult.profile.cn || loginResult.profile.username || loginResult.profile.displayName; + const fullName = loginResult.profile.cn || loginResult.profile.displayName || loginResult.profile.username; let eppnMatch = false; let user = null; From aab04f60e8fc28194cadd0864c6bf1651cf9450c Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Tue, 2 Jul 2019 11:34:31 -0300 Subject: [PATCH 05/87] [IMPROVE] Add descriptions on user data download buttons and popup info (#14852) --- app/models/server/models/ExportOperations.js | 9 +++++++++ app/ui-account/client/accountPreferences.js | 6 ++++-- packages/rocketchat-i18n/i18n/en.i18n.json | 8 ++++---- packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 4 ++-- server/methods/requestDataDownload.js | 5 +++++ 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/app/models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js index a72fdc486e20..72c4be71ca1b 100644 --- a/app/models/server/models/ExportOperations.js +++ b/app/models/server/models/ExportOperations.js @@ -46,6 +46,15 @@ export class ExportOperations extends Base { return this.find(query, options); } + findAllPendingBeforeMyRequest(requestDay, options) { + const query = { + status: { $nin: ['completed'] }, + createdAt: { $lt: requestDay }, + }; + + return this.find(query, options); + } + // UPDATE updateOperation(data) { const update = { diff --git a/app/ui-account/client/accountPreferences.js b/app/ui-account/client/accountPreferences.js index 417092de21cb..cd4058b3664d 100644 --- a/app/ui-account/client/accountPreferences.js +++ b/app/ui-account/client/accountPreferences.js @@ -235,8 +235,9 @@ Template.accountPreferences.onCreated(function() { if (results.requested) { modal.open({ title: t('UserDataDownload_Requested'), - text: t('UserDataDownload_Requested_Text'), + text: t('UserDataDownload_Requested_Text', { pending_operations: results.pendingOperationsBeforeMyRequest }), type: 'success', + html: true, }); return true; @@ -260,8 +261,9 @@ Template.accountPreferences.onCreated(function() { modal.open({ title: t('UserDataDownload_Requested'), - text: t('UserDataDownload_RequestExisted_Text'), + text: t('UserDataDownload_RequestExisted_Text', { pending_operations: results.pendingOperationsBeforeMyRequest }), type: 'success', + html: true, }); return true; } diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 5fe15369f9d5..43b9d366c7ab 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1102,7 +1102,7 @@ "Domains": "Domains", "Domains_allowed_to_embed_the_livechat_widget": "Comma-separated list of domains allowed to embed the livechat widget. Leave blank to allow all domains.", "Downloading_file_from_external_URL": "Downloading file from external URL", - "Download_My_Data": "Download My Data", + "Download_My_Data": "Download My Data (HTML)", "Download_Snippet": "Download", "Drop_to_upload_file": "Drop to upload file", "Dry_run": "Dry run", @@ -1318,7 +1318,7 @@ "except_pinned": "(except those that are pinned)", "Execute_Synchronization_Now": "Execute Synchronization Now", "Exit_Full_Screen": "Exit Full Screen", - "Export_My_Data": "Export My Data", + "Export_My_Data": "Export My Data (JSON)", "expression": "Expression", "Extended": "Extended", "External_Domains": "External Domains", @@ -3109,8 +3109,8 @@ "UserDataDownload_EmailBody": "Your data file is now ready to download. Click here to download it.", "UserDataDownload_EmailSubject": "Your Data File is Ready to Download", "UserDataDownload_Requested": "Download File Requested", - "UserDataDownload_Requested_Text": "Your data file will be generated. A link to download it will be sent to your email address when ready.", - "UserDataDownload_RequestExisted_Text": "Your data file is already being generated. A link to download it will be sent to your email address when ready.", + "UserDataDownload_Requested_Text": "Your data file will be generated. A link to download it will be sent to your email address when ready. There are __pending_operations__ queued operations to run before yours.", + "UserDataDownload_RequestExisted_Text": "Your data file is already being generated. A link to download it will be sent to your email address when ready. There are __pending_operations__ queued operations to run before yours.", "Username": "Username", "Username_already_exist": "Username already exists. Please try another username.", "Username_and_message_must_not_be_empty": "Username and message must not be empty.", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index ea5ec5fb0564..4c8164e5db32 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1067,7 +1067,7 @@ "Domains": "Domínios", "Domains_allowed_to_embed_the_livechat_widget": "A lista de domínios separados por vírgulas permitiu incorporar o widget do Livechat. Deixe em branco para permitir todos os domínios.", "Downloading_file_from_external_URL": "Baixar arquivo de URL externa", - "Download_My_Data": "Baixar meus dados", + "Download_My_Data": "Baixar meus dados (HTML)", "Download_Snippet": "Baixar", "Drop_to_upload_file": "Largue para enviar arquivos", "Dry_run": "Simulação", @@ -1279,7 +1279,7 @@ "except_pinned": "(exceto aqueles que estão presos)", "Execute_Synchronization_Now": "Execute sincronização agora", "Exit_Full_Screen": "Sair da tela cheia", - "Export_My_Data": "Exportar meus dados", + "Export_My_Data": "Exportar meus dados (JSON)", "expression": "Expressão", "Extended": "Estendido", "External_Domains": "Domínios Externos", diff --git a/server/methods/requestDataDownload.js b/server/methods/requestDataDownload.js index 255def67976a..d2eb9f20c5a8 100644 --- a/server/methods/requestDataDownload.js +++ b/server/methods/requestDataDownload.js @@ -20,6 +20,8 @@ Meteor.methods({ const userId = currentUserData._id; const lastOperation = ExportOperations.findLastOperationByUser(userId, fullExport); + const requestDay = lastOperation ? lastOperation.createdAt : new Date(); + const pendingOperationsBeforeMyRequestCount = ExportOperations.findAllPendingBeforeMyRequest(requestDay).count(); if (lastOperation) { const yesterday = new Date(); @@ -33,6 +35,7 @@ Meteor.methods({ requested: false, exportOperation: lastOperation, url: lastFile.url, + pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount, }; } } @@ -41,6 +44,7 @@ Meteor.methods({ requested: false, exportOperation: lastOperation, url: null, + pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount, }; } } @@ -81,6 +85,7 @@ Meteor.methods({ requested: true, exportOperation, url: null, + pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount, }; }, }); From a09bd7c0fc216c8a453b95d472267254b3299dc9 Mon Sep 17 00:00:00 2001 From: Zolo | Zolbayar Bayarsaikhan Date: Tue, 2 Jul 2019 22:35:31 +0800 Subject: [PATCH 06/87] [FIX] Opening Livechat messages on mobile apps (#14785) --- app/api/server/v1/channels.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js index f92132a7d4e5..1a4e74afbbcb 100644 --- a/app/api/server/v1/channels.js +++ b/app/api/server/v1/channels.js @@ -23,7 +23,7 @@ function findChannelByIdOrName({ params, checkedArchived = true, userId }) { room = Rooms.findOneByName(params.roomName, { fields }); } - if (!room || room.t !== 'c') { + if (!room || (room.t !== 'c' && room.t !== 'l')) { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any channel'); } From 7e9ba13210369ec265b811a26a4d556c36000124 Mon Sep 17 00:00:00 2001 From: vova-zush Date: Tue, 2 Jul 2019 17:36:48 +0300 Subject: [PATCH 07/87] [FIX] Video recorder message echo (#14671) --- app/ui/client/lib/recorderjs/videoRecorder.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/ui/client/lib/recorderjs/videoRecorder.js b/app/ui/client/lib/recorderjs/videoRecorder.js index 0c09d44ecc48..8b3a92c46f9a 100644 --- a/app/ui/client/lib/recorderjs/videoRecorder.js +++ b/app/ui/client/lib/recorderjs/videoRecorder.js @@ -62,6 +62,7 @@ export const VideoRecorder = new class VideoRecorder { this.videoel.src = URL.createObjectURL(stream); } + this.videoel.muted = true; this.videoel.onloadedmetadata = () => { this.videoel && this.videoel.play(); }; From 3ce57f48c11247b91c1c3ef23c8d343f21363381 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 2 Jul 2019 16:28:49 -0300 Subject: [PATCH 08/87] Extract canSendMessage function (#14909) --- .../server/functions/canSendMessage.js | 54 +++++++++---------- app/models/server/raw/BaseRaw.js | 4 ++ app/models/server/raw/Rooms.js | 26 +++++++++ app/models/server/raw/Subscriptions.js | 10 ++-- app/models/server/raw/index.js | 3 ++ 5 files changed, 62 insertions(+), 35 deletions(-) create mode 100644 app/models/server/raw/Rooms.js diff --git a/app/authorization/server/functions/canSendMessage.js b/app/authorization/server/functions/canSendMessage.js index 7ca6a41d3646..fbae305e07ac 100644 --- a/app/authorization/server/functions/canSendMessage.js +++ b/app/authorization/server/functions/canSendMessage.js @@ -1,44 +1,38 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/tap:i18n'; -import { Random } from 'meteor/random'; - -import { canAccessRoom } from './canAccessRoom'; -import { hasPermission } from './hasPermission'; -import { Notifications } from '../../../notifications'; -import { Rooms, Subscriptions } from '../../../models'; - +import { canAccessRoomAsync } from './canAccessRoom'; +import { hasPermissionAsync } from './hasPermission'; +import { Subscriptions, Rooms } from '../../../models/server/raw'; + +const subscriptionOptions = { + projection: { + blocked: 1, + blocker: 1, + }, +}; -export const canSendMessage = (rid, { uid, username }, extraData) => { - const room = Rooms.findOneById(rid); +export const canSendMessageAsync = async (rid, { uid, username }, extraData) => { + const room = await Rooms.findOneById(rid); - if (!canAccessRoom.call(this, room, { _id: uid, username }, extraData)) { - throw new Meteor.Error('error-not-allowed'); + if (!await canAccessRoomAsync(room, { _id: uid, username }, extraData)) { + throw new Error('error-not-allowed'); } - const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, uid); - if (subscription && (subscription.blocked || subscription.blocker)) { - throw new Meteor.Error('room_is_blocked'); + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, subscriptionOptions); + if (subscription.blocked || subscription.blocker) { + throw new Error('room_is_blocked'); } - if (room.ro === true) { - if (!hasPermission(Meteor.userId(), 'post-readonly', room._id)) { - // Unless the user was manually unmuted - if (!(room.unmuted || []).includes(username)) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: room._id, - ts: new Date(), - msg: TAPi18n.__('room_is_read_only'), - }); - - throw new Meteor.Error('You can\'t send messages because the room is readonly.'); - } + if (room.ro === true && !await hasPermissionAsync(uid, 'post-readonly', rid)) { + // Unless the user was manually unmuted + if (!(room.unmuted || []).includes(username)) { + throw new Error('You can\'t send messages because the room is readonly.'); } } if ((room.muted || []).includes(username)) { - throw new Meteor.Error('You_have_been_muted'); + throw new Error('You_have_been_muted'); } return room; }; + +export const canSendMessage = (rid, { uid, username }, extraData) => Promise.await(canSendMessageAsync(rid, { uid, username }, extraData)); diff --git a/app/models/server/raw/BaseRaw.js b/app/models/server/raw/BaseRaw.js index 2fe2c81767b0..a2668cca3779 100644 --- a/app/models/server/raw/BaseRaw.js +++ b/app/models/server/raw/BaseRaw.js @@ -3,6 +3,10 @@ export class BaseRaw { this.col = col; } + findOneById(_id, options) { + return this.findOne({ _id }, options); + } + findOne(...args) { return this.col.findOne(...args); } diff --git a/app/models/server/raw/Rooms.js b/app/models/server/raw/Rooms.js new file mode 100644 index 000000000000..d04e5aadee3a --- /dev/null +++ b/app/models/server/raw/Rooms.js @@ -0,0 +1,26 @@ +import { BaseRaw } from './BaseRaw'; + +export class RoomsRaw extends BaseRaw { + findOneByRoomIdAndUserId(rid, uid, options) { + const query = { + rid, + 'u._id': uid, + }; + + return this.col.findOne(query, options); + } + + isUserInRole(uid, roleName, rid) { + if (rid == null) { + return; + } + + const query = { + 'u._id': uid, + rid, + roles: roleName, + }; + + return this.findOne(query, { fields: { roles: 1 } }); + } +} diff --git a/app/models/server/raw/Subscriptions.js b/app/models/server/raw/Subscriptions.js index 79a5e0f4b10d..ac7115393b41 100644 --- a/app/models/server/raw/Subscriptions.js +++ b/app/models/server/raw/Subscriptions.js @@ -1,22 +1,22 @@ import { BaseRaw } from './BaseRaw'; export class SubscriptionsRaw extends BaseRaw { - findOneByRoomIdAndUserId(roomId, userId, options) { + findOneByRoomIdAndUserId(rid, uid, options) { const query = { - rid: roomId, - 'u._id': userId, + rid, + 'u._id': uid, }; return this.col.findOne(query, options); } - isUserInRole(userId, roleName, rid) { + isUserInRole(uid, roleName, rid) { if (rid == null) { return; } const query = { - 'u._id': userId, + 'u._id': uid, rid, roles: roleName, }; diff --git a/app/models/server/raw/index.js b/app/models/server/raw/index.js index 818f2a9c1a24..9a56a1f09e21 100644 --- a/app/models/server/raw/index.js +++ b/app/models/server/raw/index.js @@ -8,9 +8,12 @@ import SettingsModel from '../models/Settings'; import { SettingsRaw } from './Settings'; import UsersModel from '../models/Users'; import { UsersRaw } from './Users'; +import RoomsModel from '../models/Rooms'; +import { RoomsRaw } from './Rooms'; export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection()); export const Roles = new RolesRaw(RolesModel.model.rawCollection()); export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection()); export const Settings = new SettingsRaw(SettingsModel.model.rawCollection()); export const Users = new UsersRaw(UsersModel.model.rawCollection()); +export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection()); From da8775aab147f338cf366a4806f828fa77155110 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 3 Jul 2019 16:00:29 -0300 Subject: [PATCH 09/87] Split oplog emitters in files (#14917) --- .../publications/permissions/emitter.js | 24 +++++ .../{permissions.js => permissions/index.js} | 19 +--- app/lib/server/index.js | 1 - app/lib/server/publications/settings.js | 99 ------------------- app/models/server/models/_BaseDb.js | 40 ++++---- server/main.js | 1 + server/publications/room/emitter.js | 37 +++++++ .../publications/{room.js => room/index.js} | 40 ++------ server/publications/settings/emitter.js | 45 +++++++++ server/publications/settings/index.js | 58 +++++++++++ server/publications/subscription/emitter.js | 27 +++++ .../index.js} | 24 +---- server/stream/messages.js | 82 --------------- server/stream/messages/emitter.js | 39 ++++++++ server/stream/messages/index.js | 51 ++++++++++ 15 files changed, 315 insertions(+), 272 deletions(-) create mode 100644 app/authorization/server/publications/permissions/emitter.js rename app/authorization/server/publications/{permissions.js => permissions/index.js} (52%) delete mode 100644 app/lib/server/publications/settings.js create mode 100644 server/publications/room/emitter.js rename server/publications/{room.js => room/index.js} (70%) create mode 100644 server/publications/settings/emitter.js create mode 100644 server/publications/settings/index.js create mode 100644 server/publications/subscription/emitter.js rename server/publications/{subscription.js => subscription/index.js} (64%) delete mode 100644 server/stream/messages.js create mode 100644 server/stream/messages/emitter.js create mode 100644 server/stream/messages/index.js diff --git a/app/authorization/server/publications/permissions/emitter.js b/app/authorization/server/publications/permissions/emitter.js new file mode 100644 index 000000000000..e67c53d8fefc --- /dev/null +++ b/app/authorization/server/publications/permissions/emitter.js @@ -0,0 +1,24 @@ +import { Notifications } from '../../../../notifications'; +import Permissions from '../../../../models/server/models/Permissions'; + +Permissions.on('change', ({ clientAction, id, data, diff }) => { + if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { // avoid useless changes + return; + } + switch (clientAction) { + case 'updated': + case 'inserted': + data = data || Permissions.findOneById(id); + break; + + case 'removed': + data = { _id: id }; + break; + } + + Notifications.notifyLoggedInThisInstance( + 'permissions-changed', + clientAction, + data + ); +}); diff --git a/app/authorization/server/publications/permissions.js b/app/authorization/server/publications/permissions/index.js similarity index 52% rename from app/authorization/server/publications/permissions.js rename to app/authorization/server/publications/permissions/index.js index b99a9bbd5ae2..45f4f9d01c1a 100644 --- a/app/authorization/server/publications/permissions.js +++ b/app/authorization/server/publications/permissions/index.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import Permissions from '../../../models/server/models/Permissions'; -import { Notifications } from '../../../notifications'; +import Permissions from '../../../../models/server/models/Permissions'; +import './emitter'; Meteor.methods({ 'permissions/get'(updatedAt) { @@ -20,18 +20,3 @@ Meteor.methods({ return records; }, }); - -Permissions.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'updated': - case 'inserted': - data = data || Permissions.findOneById(id); - break; - - case 'removed': - data = { _id: id }; - break; - } - - Notifications.notifyLoggedInThisInstance('permissions-changed', clientAction, data); -}); diff --git a/app/lib/server/index.js b/app/lib/server/index.js index dfbf8f7b010e..aa0f7c468bfc 100644 --- a/app/lib/server/index.js +++ b/app/lib/server/index.js @@ -65,7 +65,6 @@ import './methods/setUsername'; import './methods/unarchiveRoom'; import './methods/unblockUser'; import './methods/updateMessage'; -import './publications/settings'; export * from './lib'; export * from './functions'; diff --git a/app/lib/server/publications/settings.js b/app/lib/server/publications/settings.js deleted file mode 100644 index dec4c0a6d4ae..000000000000 --- a/app/lib/server/publications/settings.js +++ /dev/null @@ -1,99 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Settings } from '../../../models'; -import { hasPermission } from '../../../authorization'; -import { Notifications } from '../../../notifications'; - -Meteor.methods({ - 'public-settings/get'(updatedAt) { - const records = Settings.findNotHiddenPublic().fetch(); - - if (updatedAt instanceof Date) { - return { - update: records.filter(function(record) { - return record._updatedAt > updatedAt; - }), - remove: Settings.trashFindDeletedAfter(updatedAt, { - hidden: { - $ne: true, - }, - public: true, - }, { - fields: { - _id: 1, - _deletedAt: 1, - }, - }).fetch(), - }; - } - return records; - }, - 'private-settings/get'(updatedAfter) { - if (!Meteor.userId()) { - return []; - } - if (!hasPermission(Meteor.userId(), 'view-privileged-setting')) { - return []; - } - - if (!(updatedAfter instanceof Date)) { - return Settings.findNotHidden().fetch(); - } - - const records = Settings.findNotHidden({ updatedAfter }).fetch(); - return { - update: records, - remove: Settings.trashFindDeletedAfter(updatedAfter, { - hidden: { - $ne: true, - }, - }, { - fields: { - _id: 1, - _deletedAt: 1, - }, - }).fetch(), - }; - }, -}); - -Settings.on('change', ({ clientAction, id, data, diff }) => { - if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { // avoid useless changes - return; - } - switch (clientAction) { - case 'updated': - case 'inserted': { - const setting = data || Settings.findOneById(id); - const value = { - _id: setting._id, - value: setting.value, - editor: setting.editor, - properties: setting.properties, - }; - - if (setting.public === true) { - Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value); - } - Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, setting); - break; - } - - case 'removed': { - const setting = data || Settings.findOneById(id, { fields: { public: 1 } }); - - if (setting && setting.public === true) { - Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, { _id: id }); - } - Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, { _id: id }); - break; - } - } -}); - -Notifications.streamAll.allowRead('private-settings-changed', function() { - if (this.userId == null) { - return false; - } - return hasPermission(this.userId, 'view-privileged-setting'); -}); diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js index cd774981ffb6..c588620187f8 100644 --- a/app/models/server/models/_BaseDb.js +++ b/app/models/server/models/_BaseDb.js @@ -14,8 +14,6 @@ try { console.log(e); } -const isOplogEnabled = MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle && !!MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry; - export class BaseDb extends EventEmitter { constructor(model, baseModel) { super(); @@ -34,24 +32,30 @@ export class BaseDb extends EventEmitter { this.wrapModel(); - let alreadyListeningToOplog = false; // When someone start listening for changes we start oplog if available - this.on('newListener', (event/* , listener*/) => { - if (event === 'change' && alreadyListeningToOplog === false) { - alreadyListeningToOplog = true; - if (isOplogEnabled) { - const query = { - collection: this.collectionName, - }; - - MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry(query, this.processOplogRecord.bind(this)); - // Meteor will handle if we have a value https://github.com/meteor/meteor/blob/5dcd0b2eb9c8bf881ffbee98bc4cb7631772c4da/packages/mongo/oplog_tailing.js#L5 - if (process.env.METEOR_OPLOG_TOO_FAR_BEHIND == null) { - MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle._defineTooFarBehind(Number.MAX_SAFE_INTEGER); - } - } + const handleListener = (event /* , listener*/) => { + if (event !== 'change') { + return; } - }); + + this.removeListener('newListener', handleListener); + + const query = { + collection: this.collectionName, + }; + + MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle.onOplogEntry( + query, + this.processOplogRecord.bind(this) + ); + // Meteor will handle if we have a value https://github.com/meteor/meteor/blob/5dcd0b2eb9c8bf881ffbee98bc4cb7631772c4da/packages/mongo/oplog_tailing.js#L5 + if (process.env.METEOR_OPLOG_TOO_FAR_BEHIND == null) { + MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle._defineTooFarBehind( + Number.MAX_SAFE_INTEGER + ); + } + }; + this.on('newListener', handleListener); this.tryEnsureIndex({ _updatedAt: 1 }); } diff --git a/server/main.js b/server/main.js index 432794430828..c977827c74a9 100644 --- a/server/main.js +++ b/server/main.js @@ -72,6 +72,7 @@ import './publications/room'; import './publications/roomFiles'; import './publications/roomFilesWithSearchText'; import './publications/roomSubscriptionsByRole'; +import './publications/settings'; import './publications/spotlight'; import './publications/subscription'; import './publications/userAutocomplete'; diff --git a/server/publications/room/emitter.js b/server/publications/room/emitter.js new file mode 100644 index 000000000000..fb8c5be1c143 --- /dev/null +++ b/server/publications/room/emitter.js @@ -0,0 +1,37 @@ +import { Rooms, Subscriptions } from '../../../app/models'; +import { Notifications } from '../../../app/notifications'; + +import { fields } from '.'; + +const getSubscriptions = (id) => { + const fields = { 'u._id': 1 }; + return Subscriptions.trashFind({ rid: id }, { fields }); +}; + +Rooms.on('change', ({ clientAction, id, data }) => { + switch (clientAction) { + case 'updated': + case 'inserted': + // Override data cuz we do not publish all fields + data = Rooms.findOneById(id, { fields }); + break; + + case 'removed': + data = { _id: id }; + break; + } + + if (data) { + if (clientAction === 'removed') { + getSubscriptions(clientAction, id).forEach(({ u }) => { + Notifications.notifyUserInThisInstance( + u._id, + 'rooms-changed', + clientAction, + data + ); + }); + } + Notifications.streamUser.__emit(id, clientAction, data); + } +}); diff --git a/server/publications/room.js b/server/publications/room/index.js similarity index 70% rename from server/publications/room.js rename to server/publications/room/index.js index 5df737755eeb..2643c68982c5 100644 --- a/server/publications/room.js +++ b/server/publications/room/index.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { roomTypes } from '../../app/utils'; -import { hasPermission } from '../../app/authorization'; -import { Rooms, Subscriptions } from '../../app/models'; -import { settings } from '../../app/settings'; -import { Notifications } from '../../app/notifications'; +import { roomTypes } from '../../../app/utils'; +import { hasPermission } from '../../../app/authorization'; +import { Rooms } from '../../../app/models'; +import { settings } from '../../../app/settings'; +import './emitter'; -const fields = { +export const fields = { _id: 1, name: 1, fname: 1, @@ -107,31 +107,3 @@ Meteor.methods({ return roomMap(room); }, }); - -const getSubscriptions = (id) => { - const fields = { 'u._id': 1 }; - return Subscriptions.trashFind({ rid: id }, { fields }); -}; - -Rooms.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'updated': - case 'inserted': - // Override data cuz we do not publish all fields - data = Rooms.findOneById(id, { fields }); - break; - - case 'removed': - data = { _id: id }; - break; - } - - if (data) { - if (clientAction === 'removed') { - getSubscriptions(clientAction, id).forEach(({ u }) => { - Notifications.notifyUserInThisInstance(u._id, 'rooms-changed', clientAction, data); - }); - } - Notifications.streamUser.__emit(id, clientAction, data); - } -}); diff --git a/server/publications/settings/emitter.js b/server/publications/settings/emitter.js new file mode 100644 index 000000000000..2a9312b22548 --- /dev/null +++ b/server/publications/settings/emitter.js @@ -0,0 +1,45 @@ +import { Settings } from '../../../app/models'; +import { Notifications } from '../../../app/notifications'; +import { hasPermission } from '../../../app/authorization'; + +Settings.on('change', ({ clientAction, id, data, diff }) => { + if (diff && Object.keys(diff).length === 1 && diff._updatedAt) { // avoid useless changes + return; + } + switch (clientAction) { + case 'updated': + case 'inserted': { + const setting = data || Settings.findOneById(id); + const value = { + _id: setting._id, + value: setting.value, + editor: setting.editor, + properties: setting.properties, + }; + + if (setting.public === true) { + Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, value); + } + Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, setting); + break; + } + + case 'removed': { + const setting = data || Settings.findOneById(id, { fields: { public: 1 } }); + + if (setting && setting.public === true) { + Notifications.notifyAllInThisInstance('public-settings-changed', clientAction, { _id: id }); + } + Notifications.notifyLoggedInThisInstance('private-settings-changed', clientAction, { _id: id }); + break; + } + } +}); + + +Notifications.streamAll.allowRead('private-settings-changed', function() { + if (this.userId == null) { + return false; + } + return hasPermission(this.userId, 'view-privileged-setting'); +}); diff --git a/server/publications/settings/index.js b/server/publications/settings/index.js new file mode 100644 index 000000000000..5c89f3fff50f --- /dev/null +++ b/server/publications/settings/index.js @@ -0,0 +1,58 @@ +import { Meteor } from 'meteor/meteor'; + +import { Settings } from '../../../app/models'; +import { hasPermission } from '../../../app/authorization'; +import './emitter'; + +Meteor.methods({ + 'public-settings/get'(updatedAt) { + const records = Settings.findNotHiddenPublic().fetch(); + + if (updatedAt instanceof Date) { + return { + update: records.filter(function(record) { + return record._updatedAt > updatedAt; + }), + remove: Settings.trashFindDeletedAfter(updatedAt, { + hidden: { + $ne: true, + }, + public: true, + }, { + fields: { + _id: 1, + _deletedAt: 1, + }, + }).fetch(), + }; + } + return records; + }, + 'private-settings/get'(updatedAfter) { + if (!Meteor.userId()) { + return []; + } + if (!hasPermission(Meteor.userId(), 'view-privileged-setting')) { + return []; + } + + if (!(updatedAfter instanceof Date)) { + return Settings.findNotHidden().fetch(); + } + + const records = Settings.findNotHidden({ updatedAfter }).fetch(); + return { + update: records, + remove: Settings.trashFindDeletedAfter(updatedAfter, { + hidden: { + $ne: true, + }, + }, { + fields: { + _id: 1, + _deletedAt: 1, + }, + }).fetch(), + }; + }, +}); diff --git a/server/publications/subscription/emitter.js b/server/publications/subscription/emitter.js new file mode 100644 index 000000000000..f6635543c16b --- /dev/null +++ b/server/publications/subscription/emitter.js @@ -0,0 +1,27 @@ +import { Notifications } from '../../../app/notifications'; +import { Subscriptions } from '../../../app/models'; + +import { fields } from '.'; + +Subscriptions.on('change', ({ clientAction, id, data }) => { + switch (clientAction) { + case 'inserted': + case 'updated': + // Override data cuz we do not publish all fields + data = Subscriptions.findOneById(id, { fields }); + break; + + case 'removed': + data = Subscriptions.trashFindOneById(id, { fields: { u: 1, rid: 1 } }); + break; + } + + Notifications.streamUser.__emit(data.u._id, clientAction, data); + + Notifications.notifyUserInThisInstance( + data.u._id, + 'subscriptions-changed', + clientAction, + data + ); +}); diff --git a/server/publications/subscription.js b/server/publications/subscription/index.js similarity index 64% rename from server/publications/subscription.js rename to server/publications/subscription/index.js index 182de0eaf38b..8001eb21eeaf 100644 --- a/server/publications/subscription.js +++ b/server/publications/subscription/index.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions } from '../../app/models'; -import { Notifications } from '../../app/notifications'; +import { Subscriptions } from '../../../app/models'; +import './emitter'; -const fields = { +export const fields = { t: 1, ts: 1, ls: 1, @@ -70,21 +70,3 @@ Meteor.methods({ return records; }, }); - -Subscriptions.on('change', ({ clientAction, id, data }) => { - switch (clientAction) { - case 'inserted': - case 'updated': - // Override data cuz we do not publish all fields - data = Subscriptions.findOneById(id, { fields }); - break; - - case 'removed': - data = Subscriptions.trashFindOneById(id, { fields: { u: 1, rid: 1 } }); - break; - } - - Notifications.streamUser.__emit(data.u._id, clientAction, data); - - Notifications.notifyUserInThisInstance(data.u._id, 'subscriptions-changed', clientAction, data); -}); diff --git a/server/stream/messages.js b/server/stream/messages.js deleted file mode 100644 index d04f155a9e3b..000000000000 --- a/server/stream/messages.js +++ /dev/null @@ -1,82 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../app/authorization'; -import { settings } from '../../app/settings'; -import { Subscriptions, Users, Messages } from '../../app/models'; -import { msgStream } from '../../app/lib'; - -const MY_MESSAGE = '__my_messages__'; - -msgStream.allowWrite('none'); - -msgStream.allowRead(function(eventName, args) { - try { - const room = Meteor.call('canAccessRoom', eventName, this.userId, args); - - if (!room) { - return false; - } - - if (room.t === 'c' && !hasPermission(this.userId, 'preview-c-room') && !Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } })) { - return false; - } - - return true; - } catch (error) { - /* error*/ - return false; - } -}); - -msgStream.allowRead(MY_MESSAGE, 'all'); - -msgStream.allowEmit(MY_MESSAGE, function(eventName, msg) { - try { - const room = Meteor.call('canAccessRoom', msg.rid, this.userId); - - if (!room) { - return false; - } - - return { - roomParticipant: Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } }) != null, - roomType: room.t, - roomName: room.name, - }; - } catch (error) { - /* error*/ - return false; - } -}); - -Meteor.startup(function() { - function publishMessage(type, record) { - if (record._hidden !== true && (record.imported == null)) { - const UI_Use_Real_Name = settings.get('UI_Use_Real_Name') === true; - - if (record.u && record.u._id && UI_Use_Real_Name) { - const user = Users.findOneById(record.u._id); - record.u.name = user && user.name; - } - - if (record.mentions && record.mentions.length && UI_Use_Real_Name) { - record.mentions.forEach((mention) => { - const user = Users.findOneById(mention._id); - mention.name = user && user.name; - }); - } - msgStream.mymessage(MY_MESSAGE, record); - msgStream.emitWithoutBroadcast(record.rid, record); - } - } - - return Messages.on('change', function({ clientAction, id, data/* , oplog*/ }) { - switch (clientAction) { - case 'inserted': - case 'updated': - const message = data || Messages.findOne({ _id: id }); - publishMessage(clientAction, message); - break; - } - }); -}); diff --git a/server/stream/messages/emitter.js b/server/stream/messages/emitter.js new file mode 100644 index 000000000000..aa1f7260d917 --- /dev/null +++ b/server/stream/messages/emitter.js @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../app/settings'; +import { Users, Messages } from '../../../app/models'; +import { msgStream } from '../../../app/lib/server'; + +import { MY_MESSAGE } from '.'; + +Meteor.startup(function() { + function publishMessage(type, record) { + if (record._hidden !== true && (record.imported == null)) { + const UI_Use_Real_Name = settings.get('UI_Use_Real_Name') === true; + + if (record.u && record.u._id && UI_Use_Real_Name) { + const user = Users.findOneById(record.u._id); + record.u.name = user && user.name; + } + + if (record.mentions && record.mentions.length && UI_Use_Real_Name) { + record.mentions.forEach((mention) => { + const user = Users.findOneById(mention._id); + mention.name = user && user.name; + }); + } + msgStream.mymessage(MY_MESSAGE, record); + msgStream.emitWithoutBroadcast(record.rid, record); + } + } + + return Messages.on('change', function({ clientAction, id, data/* , oplog*/ }) { + switch (clientAction) { + case 'inserted': + case 'updated': + const message = data || Messages.findOne({ _id: id }); + publishMessage(clientAction, message); + break; + } + }); +}); diff --git a/server/stream/messages/index.js b/server/stream/messages/index.js new file mode 100644 index 000000000000..9c5e839cf1c7 --- /dev/null +++ b/server/stream/messages/index.js @@ -0,0 +1,51 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../app/authorization'; +import { Subscriptions } from '../../../app/models'; +import { msgStream } from '../../../app/lib/server'; +import './emitter'; + + +export const MY_MESSAGE = '__my_messages__'; + +msgStream.allowWrite('none'); + +msgStream.allowRead(function(eventName, args) { + try { + const room = Meteor.call('canAccessRoom', eventName, this.userId, args); + + if (!room) { + return false; + } + + if (room.t === 'c' && !hasPermission(this.userId, 'preview-c-room') && !Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } })) { + return false; + } + + return true; + } catch (error) { + /* error*/ + return false; + } +}); + +msgStream.allowRead(MY_MESSAGE, 'all'); + +msgStream.allowEmit(MY_MESSAGE, function(eventName, msg) { + try { + const room = Meteor.call('canAccessRoom', msg.rid, this.userId); + + if (!room) { + return false; + } + + return { + roomParticipant: Subscriptions.findOneByRoomIdAndUserId(room._id, this.userId, { fields: { _id: 1 } }) != null, + roomType: room.t, + roomName: room.name, + }; + } catch (error) { + /* error*/ + return false; + } +}); From 3661c63872caaebcfec87821eb684b951e2b88cb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 3 Jul 2019 21:46:28 -0300 Subject: [PATCH 10/87] Callbacks perf (#14915) Co-Authored-By: Diego Sampaio Co-Authored-By: Tasso Evangelista --- app/2fa/server/loginHandler.js | 2 +- app/callbacks/lib/callbacks.js | 132 ++++++++++-------- .../server/hooks/joinDiscussionOnMessage.js | 2 +- app/dolphin/lib/common.js | 2 +- app/e2e/server/index.js | 2 +- app/emoji-emojione/server/callbacks.js | 2 +- app/google-vision/server/googlevision.js | 2 +- .../resolvers/messages/chatMessageAdded.js | 2 +- app/integrations/server/triggers.js | 16 +-- app/livechat/server/hooks/externalMessage.js | 47 +++---- .../server/hooks/markRoomResponded.js | 13 +- .../server/hooks/saveAnalyticsData.js | 82 ++++++----- app/metrics/server/callbacksMetrics.js | 7 +- app/search/server/events/events.js | 4 +- imports/message-read-receipt/server/hooks.js | 4 +- 15 files changed, 164 insertions(+), 155 deletions(-) diff --git a/app/2fa/server/loginHandler.js b/app/2fa/server/loginHandler.js index 3afc20b6cf21..a70f582f946b 100644 --- a/app/2fa/server/loginHandler.js +++ b/app/2fa/server/loginHandler.js @@ -36,4 +36,4 @@ callbacks.add('onValidateLogin', (login) => { throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); } } -}); +}, callbacks.priority.MEDIUM, '2fa'); diff --git a/app/callbacks/lib/callbacks.js b/app/callbacks/lib/callbacks.js index 589764076924..193b6be06ed6 100644 --- a/app/callbacks/lib/callbacks.js +++ b/app/callbacks/lib/callbacks.js @@ -2,6 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import _ from 'underscore'; +let timed = false; + +if (Meteor.isClient) { + const { getConfig } = require('../../ui-utils/client/config'); + timed = [getConfig('debug'), getConfig('timed-callbacks')].includes('true'); +} /* * Callback hooks provide an easy way to add extra steps to common operations. * @namespace RocketChat.callbacks @@ -9,15 +15,48 @@ import _ from 'underscore'; export const callbacks = {}; -if (Meteor.isServer) { - callbacks.showTime = true; - callbacks.showTotalTime = true; -} else { - callbacks.showTime = false; - callbacks.showTotalTime = false; -} +const wrapCallback = (callback) => (...args) => { + const time = Date.now(); + const result = callback(...args); + const currentTime = Date.now() - time; + let stack = callback.stack + && typeof callback.stack.split === 'function' + && callback.stack.split('\n'); + stack = stack && stack[2] && (stack[2].match(/\(.+\)/) || [])[0]; + console.log(String(currentTime), callback.hook, callback.id, stack); + return result; +}; + +const wrapRun = (hook, fn) => (...args) => { + const time = Date.now(); + const ret = fn(...args); + const totalTime = Date.now() - time; + console.log(`${ hook }:`, totalTime); + return ret; +}; + +const handleResult = (fn) => (result, constant) => { + const callbackResult = callbacks.runItem({ hook: fn.hook, callback: fn, result, constant }); + return typeof callbackResult === 'undefined' ? result : callbackResult; +}; +const identity = (e) => e; +const pipe = (f, g) => (e, ...constants) => g(f(e, ...constants), ...constants); +const createCallback = (hook, callbacks) => callbacks.map(handleResult).reduce(pipe, identity); + +const createCallbackTimed = (hook, callbacks) => + wrapRun(hook, + callbacks + .map(wrapCallback) + .map(handleResult) + .reduce(pipe, identity) + ); + +const create = (hook, cbs) => + (timed ? createCallbackTimed(hook, cbs) : createCallback(hook, cbs)); +const combinedCallbacks = new Map(); +this.combinedCallbacks = combinedCallbacks; /* * Callback priorities */ @@ -36,26 +75,24 @@ const getHooks = (hookName) => callbacks[hookName] || []; * @param {Function} callback - The callback function */ -callbacks.add = function(hook, callback, priority, id = Random.id()) { - if (!_.isNumber(priority)) { - priority = callbacks.priority.MEDIUM; +callbacks.add = function( + hook, + callback, + priority = callbacks.priority.MEDIUM, + id = Random.id() +) { + callbacks[hook] = getHooks(hook); + if (callbacks[hook].find((cb) => cb.id === id)) { + return; } + callback.hook = hook; callback.priority = priority; callback.id = id; - callbacks[hook] = getHooks(hook); - - if (callbacks.showTime === true) { - const err = new Error(); - callback.stack = err.stack; - } + callback.stack = new Error().stack; - if (callbacks[hook].find((cb) => cb.id === callback.id)) { - return; - } callbacks[hook].push(callback); - callbacks[hook] = _.sortBy(callbacks[hook], function(callback) { - return callback.priority || callbacks.priority.MEDIUM; - }); + callbacks[hook] = _.sortBy(callbacks[hook], (callback) => callback.priority || callbacks.priority.MEDIUM); + combinedCallbacks.set(hook, create(hook, callbacks[hook])); }; @@ -67,11 +104,10 @@ callbacks.add = function(hook, callback, priority, id = Random.id()) { callbacks.remove = function(hook, id) { callbacks[hook] = getHooks(hook).filter((callback) => callback.id !== id); + combinedCallbacks.set(hook, create(hook, callbacks[hook])); }; -callbacks.runItem = function({ callback, result, constant /* , hook */ }) { - return callback(result, constant); -}; +callbacks.runItem = ({ callback, result, constant /* , hook */ }) => callback(result, constant); /* * Successively run all of a hook's callbacks on an item @@ -82,38 +118,18 @@ callbacks.runItem = function({ callback, result, constant /* , hook */ }) { */ callbacks.run = function(hook, item, constant) { - const callbackItems = callbacks[hook]; - if (!callbackItems || !callbackItems.length) { + const runner = combinedCallbacks.get(hook); + if (!runner) { return item; } - let totalTime = 0; - const result = callbackItems.reduce(function(result, callback) { - const time = callbacks.showTime === true || callbacks.showTotalTime === true ? Date.now() : 0; - - const callbackResult = callbacks.runItem({ hook, callback, result, constant, time }); - - if (callbacks.showTime === true || callbacks.showTotalTime === true) { - const currentTime = Date.now() - time; - totalTime += currentTime; - if (callbacks.showTime === true) { - if (!Meteor.isServer) { - let stack = callback.stack && typeof callback.stack.split === 'function' && callback.stack.split('\n'); - stack = stack && stack[2] && (stack[2].match(/\(.+\)/) || [])[0]; - console.log(String(currentTime), hook, callback.id, stack); - } - } - } - return typeof callbackResult === 'undefined' ? result : callbackResult; - }, item); - - if (callbacks.showTotalTime === true) { - if (!Meteor.isServer) { - console.log(`${ hook }:`, totalTime); - } - } + return runner(item, constant); - return result; + // return callbackItems.reduce(function(result, callback) { + // const callbackResult = callbacks.runItem({ hook, callback, result, constant }); + + // return typeof callbackResult === 'undefined' ? result : callbackResult; + // }, item); }; @@ -124,12 +140,10 @@ callbacks.run = function(hook, item, constant) { * @param {Object} [constant] - An optional constant that will be passed along to each callback */ -callbacks.runAsync = function(hook, item, constant) { +callbacks.runAsync = Meteor.isServer ? function(hook, item, constant) { const callbackItems = callbacks[hook]; - if (Meteor.isServer && callbackItems && callbackItems.length) { - Meteor.defer(function() { - callbackItems.forEach((callback) => callback(item, constant)); - }); + if (callbackItems && callbackItems.length) { + callbackItems.forEach((callback) => Meteor.defer(function() { callback(item, constant); })); } return item; -}; +} : () => { throw new Error('callbacks.runAsync on client server not allowed'); }; diff --git a/app/discussion/server/hooks/joinDiscussionOnMessage.js b/app/discussion/server/hooks/joinDiscussionOnMessage.js index 08eb79071dc4..4884d47e1346 100644 --- a/app/discussion/server/hooks/joinDiscussionOnMessage.js +++ b/app/discussion/server/hooks/joinDiscussionOnMessage.js @@ -19,4 +19,4 @@ callbacks.add('beforeSaveMessage', (message, room) => { Meteor.runAsUser(message.u._id, () => Meteor.call('joinRoom', room._id)); return message; -}); +}, callbacks.priority.MEDIUM, 'joinDiscussionOnMessage'); diff --git a/app/dolphin/lib/common.js b/app/dolphin/lib/common.js index 36c95452a975..6f88e4c8b149 100644 --- a/app/dolphin/lib/common.js +++ b/app/dolphin/lib/common.js @@ -57,7 +57,7 @@ if (Meteor.isServer) { ServiceConfiguration.configurations.upsert({ service: 'dolphin' }, { $set: data }); } - callbacks.add('beforeCreateUser', DolphinOnCreateUser, callbacks.priority.HIGH); + callbacks.add('beforeCreateUser', DolphinOnCreateUser, callbacks.priority.HIGH, 'dolphin'); } else { Meteor.startup(() => Tracker.autorun(function() { diff --git a/app/e2e/server/index.js b/app/e2e/server/index.js index fdf3db07c92d..c9cfc3faca30 100644 --- a/app/e2e/server/index.js +++ b/app/e2e/server/index.js @@ -12,4 +12,4 @@ import './methods/requestSubscriptionKeys'; callbacks.add('afterJoinRoom', (user, room) => { Notifications.notifyRoom('e2e.keyRequest', room._id, room.e2eKeyId); -}); +}, callbacks.priority.MEDIUM, 'e2e'); diff --git a/app/emoji-emojione/server/callbacks.js b/app/emoji-emojione/server/callbacks.js index fb88919f10b0..cd06854f3023 100644 --- a/app/emoji-emojione/server/callbacks.js +++ b/app/emoji-emojione/server/callbacks.js @@ -4,5 +4,5 @@ import emojione from 'emojione'; import { callbacks } from '../../callbacks'; Meteor.startup(function() { - callbacks.add('beforeSendMessageNotifications', (message) => emojione.shortnameToUnicode(message)); + callbacks.add('beforeSendMessageNotifications', (message) => emojione.shortnameToUnicode(message), callbacks.priority.MEDIUM, 'emojione-shortnameToUnicode'); }); diff --git a/app/google-vision/server/googlevision.js b/app/google-vision/server/googlevision.js index 658ef00f2631..35717a623243 100644 --- a/app/google-vision/server/googlevision.js +++ b/app/google-vision/server/googlevision.js @@ -35,7 +35,7 @@ class GoogleVision { callbacks.remove('beforeSaveMessage', 'googlevision-blockunsafe'); } }); - callbacks.add('afterFileUpload', this.annotate.bind(this)); + callbacks.add('afterFileUpload', this.annotate.bind(this), callbacks.priority.MEDIUM, 'GoogleVision'); } incCallCount(count) { diff --git a/app/graphql/server/resolvers/messages/chatMessageAdded.js b/app/graphql/server/resolvers/messages/chatMessageAdded.js index f8e3e54c455a..dc8ee0ddba7c 100644 --- a/app/graphql/server/resolvers/messages/chatMessageAdded.js +++ b/app/graphql/server/resolvers/messages/chatMessageAdded.js @@ -44,7 +44,7 @@ const resolver = { callbacks.add('afterSaveMessage', (message) => { publishMessage(message); -}, null, 'chatMessageAddedSubscription'); +}, callbacks.priority.MEDIUM, 'chatMessageAddedSubscription'); export { schema, diff --git a/app/integrations/server/triggers.js b/app/integrations/server/triggers.js index 576e7f1801af..c7c39c50cd5c 100644 --- a/app/integrations/server/triggers.js +++ b/app/integrations/server/triggers.js @@ -7,11 +7,11 @@ const callbackHandler = function _callbackHandler(eventType) { }; }; -callbacks.add('afterSaveMessage', callbackHandler('sendMessage'), callbacks.priority.LOW); -callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), callbacks.priority.LOW); -callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), callbacks.priority.LOW); -callbacks.add('afterCreateUser', callbackHandler('userCreated'), callbacks.priority.LOW); -callbacks.add('afterJoinRoom', callbackHandler('roomJoined'), callbacks.priority.LOW); -callbacks.add('afterLeaveRoom', callbackHandler('roomLeft'), callbacks.priority.LOW); -callbacks.add('afterRoomArchived', callbackHandler('roomArchived'), callbacks.priority.LOW); -callbacks.add('afterFileUpload', callbackHandler('fileUploaded'), callbacks.priority.LOW); +callbacks.add('afterSaveMessage', callbackHandler('sendMessage'), callbacks.priority.LOW, 'integrations-sendMessage'); +callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); +callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); +callbacks.add('afterCreateUser', callbackHandler('userCreated'), callbacks.priority.LOW, 'integrations-userCreated'); +callbacks.add('afterJoinRoom', callbackHandler('roomJoined'), callbacks.priority.LOW, 'integrations-roomJoined'); +callbacks.add('afterLeaveRoom', callbackHandler('roomLeft'), callbacks.priority.LOW, 'integrations-roomLeft'); +callbacks.add('afterRoomArchived', callbackHandler('roomArchived'), callbacks.priority.LOW, 'integrations-roomArchived'); +callbacks.add('afterFileUpload', callbackHandler('fileUploaded'), callbacks.priority.LOW, 'integrations-fileUploaded'); diff --git a/app/livechat/server/hooks/externalMessage.js b/app/livechat/server/hooks/externalMessage.js index 91b9448be491..2ef76fbb88f1 100644 --- a/app/livechat/server/hooks/externalMessage.js +++ b/app/livechat/server/hooks/externalMessage.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; import _ from 'underscore'; @@ -39,32 +38,30 @@ callbacks.add('afterSaveMessage', function(message, room) { return message; } - Meteor.defer(() => { - try { - const response = HTTP.post('https://api.api.ai/api/query?v=20150910', { - data: { - query: message.msg, - lang: apiaiLanguage, - sessionId: room._id, - }, - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Bearer ${ apiaiKey }`, - }, - }); + try { + const response = HTTP.post('https://api.api.ai/api/query?v=20150910', { + data: { + query: message.msg, + lang: apiaiLanguage, + sessionId: room._id, + }, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Authorization: `Bearer ${ apiaiKey }`, + }, + }); - if (response.data && response.data.status.code === 200 && !_.isEmpty(response.data.result.fulfillment.speech)) { - LivechatExternalMessage.insert({ - rid: message.rid, - msg: response.data.result.fulfillment.speech, - orig: message._id, - ts: new Date(), - }); - } - } catch (e) { - SystemLogger.error('Error using Api.ai ->', e); + if (response.data && response.data.status.code === 200 && !_.isEmpty(response.data.result.fulfillment.speech)) { + LivechatExternalMessage.insert({ + rid: message.rid, + msg: response.data.result.fulfillment.speech, + orig: message._id, + ts: new Date(), + }); } - }); + } catch (e) { + SystemLogger.error('Error using Api.ai ->', e); + } return message; }, callbacks.priority.LOW, 'externalWebHook'); diff --git a/app/livechat/server/hooks/markRoomResponded.js b/app/livechat/server/hooks/markRoomResponded.js index 4f629f901aaf..ed1be72106e6 100644 --- a/app/livechat/server/hooks/markRoomResponded.js +++ b/app/livechat/server/hooks/markRoomResponded.js @@ -1,4 +1,3 @@ -import { Meteor } from 'meteor/meteor'; import { callbacks } from '../../../callbacks'; import { Rooms } from '../../../models'; @@ -19,13 +18,11 @@ callbacks.add('afterSaveMessage', function(message, room) { return message; } - Meteor.defer(() => { - Rooms.setResponseByRoomId(room._id, { - user: { - _id: message.u._id, - username: message.u.username, - }, - }); + Rooms.setResponseByRoomId(room._id, { + user: { + _id: message.u._id, + username: message.u.username, + }, }); return message; diff --git a/app/livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js index 582fa0d9cc37..58bed9f144d4 100644 --- a/app/livechat/server/hooks/saveAnalyticsData.js +++ b/app/livechat/server/hooks/saveAnalyticsData.js @@ -1,5 +1,3 @@ -import { Meteor } from 'meteor/meteor'; - import { callbacks } from '../../../callbacks'; import { Rooms } from '../../../models'; @@ -14,54 +12,54 @@ callbacks.add('afterSaveMessage', function(message, room) { return message; } - Meteor.defer(() => { - const now = new Date(); - let analyticsData; - // if the message has a token, it was sent by the visitor - if (!message.token) { - const visitorLastQuery = room.metrics && room.metrics.v ? room.metrics.v.lq : room.ts; - const agentLastReply = room.metrics && room.metrics.servedBy ? room.metrics.servedBy.lr : room.ts; - const agentJoinTime = room.servedBy && room.servedBy.ts ? room.servedBy.ts : room.ts; + const now = new Date(); + let analyticsData; + + // if the message has a token, it was sent by the visitor + if (!message.token) { + const visitorLastQuery = room.metrics && room.metrics.v ? room.metrics.v.lq : room.ts; + const agentLastReply = room.metrics && room.metrics.servedBy ? room.metrics.servedBy.lr : room.ts; + const agentJoinTime = room.servedBy && room.servedBy.ts ? room.servedBy.ts : room.ts; - const isResponseTt = room.metrics && room.metrics.response && room.metrics.response.tt; - const isResponseTotal = room.metrics && room.metrics.response && room.metrics.response.total; + const isResponseTt = room.metrics && room.metrics.response && room.metrics.response.tt; + const isResponseTotal = room.metrics && room.metrics.response && room.metrics.response.total; - if (agentLastReply === room.ts) { // first response - const firstResponseDate = now; - const firstResponseTime = (now.getTime() - visitorLastQuery) / 1000; - const responseTime = (now.getTime() - visitorLastQuery) / 1000; - const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1); + if (agentLastReply === room.ts) { // first response + const firstResponseDate = now; + const firstResponseTime = (now.getTime() - visitorLastQuery) / 1000; + const responseTime = (now.getTime() - visitorLastQuery) / 1000; + const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1); - const firstReactionDate = now; - const firstReactionTime = (now.getTime() - agentJoinTime) / 1000; - const reactionTime = (now.getTime() - agentJoinTime) / 1000; + const firstReactionDate = now; + const firstReactionTime = (now.getTime() - agentJoinTime) / 1000; + const reactionTime = (now.getTime() - agentJoinTime) / 1000; - analyticsData = { - firstResponseDate, - firstResponseTime, - responseTime, - avgResponseTime, - firstReactionDate, - firstReactionTime, - reactionTime, - }; - } else if (visitorLastQuery > agentLastReply) { // response, not first - const responseTime = (now.getTime() - visitorLastQuery) / 1000; - const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1); + analyticsData = { + firstResponseDate, + firstResponseTime, + responseTime, + avgResponseTime, + firstReactionDate, + firstReactionTime, + reactionTime, + }; + } else if (visitorLastQuery > agentLastReply) { // response, not first + const responseTime = (now.getTime() - visitorLastQuery) / 1000; + const avgResponseTime = ((isResponseTt ? room.metrics.response.tt : 0) + responseTime) / ((isResponseTotal ? room.metrics.response.total : 0) + 1); - const reactionTime = (now.getTime() - visitorLastQuery) / 1000; + const reactionTime = (now.getTime() - visitorLastQuery) / 1000; + + analyticsData = { + responseTime, + avgResponseTime, + reactionTime, + }; + } // ignore, its continuing response + } - analyticsData = { - responseTime, - avgResponseTime, - reactionTime, - }; - } // ignore, its continuing response - } + Rooms.saveAnalyticsDataByRoomId(room, message, analyticsData); - Rooms.saveAnalyticsDataByRoomId(room, message, analyticsData); - }); return message; }, callbacks.priority.LOW, 'saveAnalyticsData'); diff --git a/app/metrics/server/callbacksMetrics.js b/app/metrics/server/callbacksMetrics.js index 8402f6fa9f8e..86221f5d55f2 100644 --- a/app/metrics/server/callbacksMetrics.js +++ b/app/metrics/server/callbacksMetrics.js @@ -18,12 +18,15 @@ callbacks.run = function(hook, item, constant) { return result; }; -callbacks.runItem = function({ callback, result, constant, hook, time }) { +callbacks.runItem = function({ callback, result, constant, hook, time = Date.now() }) { const rocketchatCallbacksEnd = metrics.rocketchatCallbacks.startTimer({ hook, callback: callback.id }); const newResult = originalRunItem({ callback, result, constant }); - StatsTracker.timing('callbacks.time', Date.now() - time, [`hook:${ hook }`, `callback:${ callback.id }`]); + StatsTracker.timing('callbacks.time', Date.now() - time, [ + `hook:${ hook }`, + `callback:${ callback.id }`, + ]); rocketchatCallbacksEnd(); diff --git a/app/search/server/events/events.js b/app/search/server/events/events.js index 82fac3527e87..79e3c8aa8142 100644 --- a/app/search/server/events/events.js +++ b/app/search/server/events/events.js @@ -24,11 +24,11 @@ const eventService = new EventService(); */ callbacks.add('afterSaveMessage', function(m) { eventService.promoteEvent('message.save', m._id, m); -}); +}, callbacks.priority.MEDIUM, 'search-events'); callbacks.add('afterDeleteMessage', function(m) { eventService.promoteEvent('message.delete', m._id); -}); +}, callbacks.priority.MEDIUM, 'search-events-delete'); /** * Listen to user and room changes via cursor diff --git a/imports/message-read-receipt/server/hooks.js b/imports/message-read-receipt/server/hooks.js index eee2b1f3d795..6d2242a59cf0 100644 --- a/imports/message-read-receipt/server/hooks.js +++ b/imports/message-read-receipt/server/hooks.js @@ -15,8 +15,8 @@ callbacks.add('afterSaveMessage', (message, room) => { // mark message as read as well ReadReceipt.markMessageAsReadBySender(message, room._id, message.u._id); -}); +}, callbacks.priority.MEDIUM, 'message-read-receipt-afterSaveMessage'); callbacks.add('afterReadMessages', (rid, { userId, lastSeen }) => { ReadReceipt.markMessagesAsRead(rid, userId, lastSeen); -}); +}, callbacks.priority.MEDIUM, 'message-read-receipt-afterReadMessages'); From da42a954bfee046aebdeff595f9e71af24b376f4 Mon Sep 17 00:00:00 2001 From: Renato Becker Date: Thu, 4 Jul 2019 17:40:42 -0300 Subject: [PATCH 11/87] Adds a new setting to prevent Livechat agents from activating service status when office hours are closed. (#14921) --- app/livechat/client/index.js | 1 - .../client/methods/changeLivechatStatus.js | 15 ---------- .../client/views/app/livechatOfficeHours.html | 8 +++++ .../client/views/app/livechatOfficeHours.js | 14 +++++++++ app/livechat/server/config.js | 8 +++++ app/livechat/server/lib/Livechat.js | 29 ++++++++++++++++++- .../server/methods/changeLivechatStatus.js | 4 +++ packages/rocketchat-i18n/i18n/en.i18n.json | 2 ++ packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 2 ++ 9 files changed, 66 insertions(+), 17 deletions(-) delete mode 100644 app/livechat/client/methods/changeLivechatStatus.js diff --git a/app/livechat/client/index.js b/app/livechat/client/index.js index 5e48aa915937..6eff1d3748e5 100644 --- a/app/livechat/client/index.js +++ b/app/livechat/client/index.js @@ -3,7 +3,6 @@ import '../lib/LivechatExternalMessage'; import './roomType'; import './route'; import './ui'; -import './methods/changeLivechatStatus'; import './startup/notifyUnreadRooms'; import './views/app/analytics/livechatAnalytics'; import './views/app/analytics/livechatAnalyticsCustomDaterange'; diff --git a/app/livechat/client/methods/changeLivechatStatus.js b/app/livechat/client/methods/changeLivechatStatus.js deleted file mode 100644 index d98d97ca9038..000000000000 --- a/app/livechat/client/methods/changeLivechatStatus.js +++ /dev/null @@ -1,15 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -Meteor.methods({ - 'livechat:changeLivechatStatus'() { - if (!Meteor.userId()) { - return false; - } - - const user = Meteor.user(); - - const newStatus = user.statusLivechat === 'available' ? 'not-available' : 'available'; - - Meteor.users.update(user._id, { $set: { statusLivechat: newStatus } }); - }, -}); diff --git a/app/livechat/client/views/app/livechatOfficeHours.html b/app/livechat/client/views/app/livechatOfficeHours.html index 4c949f4f42c9..5aab0f332687 100644 --- a/app/livechat/client/views/app/livechatOfficeHours.html +++ b/app/livechat/client/views/app/livechatOfficeHours.html @@ -11,6 +11,14 @@ +
+ {{_ "Allow_Online_Agents_Outside_Office_Hours"}} + + + + +
+
{{_ "Open_days_of_the_week"}} diff --git a/app/livechat/client/views/app/livechatOfficeHours.js b/app/livechat/client/views/app/livechatOfficeHours.js index 34de2ee6d02a..870128a2824e 100644 --- a/app/livechat/client/views/app/livechatOfficeHours.js +++ b/app/livechat/client/views/app/livechatOfficeHours.js @@ -45,6 +45,16 @@ Template.livechatOfficeHours.helpers({ return 'checked'; } }, + allowAgentsOnlineOutOfficeHoursTrueChecked() { + if (Template.instance().allowAgentsOnlineOutOfficeHours.get()) { + return 'checked'; + } + }, + allowAgentsOnlineOutOfficeHoursFalseChecked() { + if (!Template.instance().allowAgentsOnlineOutOfficeHours.get()) { + return 'checked'; + } + }, }); Template.livechatOfficeHours.events({ @@ -97,6 +107,8 @@ Template.livechatOfficeHours.events({ } } + settings.set('Livechat_allow_online_agents_outside_office_hours', instance.allowAgentsOnlineOutOfficeHours.get()); + settings.set('Livechat_enable_office_hours', instance.enableOfficeHours.get(), (err/* , success*/) => { if (err) { return handleError(err); @@ -158,8 +170,10 @@ Template.livechatOfficeHours.onCreated(function() { }); this.enableOfficeHours = new ReactiveVar(null); + this.allowAgentsOnlineOutOfficeHours = new ReactiveVar(null); this.autorun(() => { this.enableOfficeHours.set(settings.get('Livechat_enable_office_hours')); + this.allowAgentsOnlineOutOfficeHours.set(settings.get('Livechat_allow_online_agents_outside_office_hours')); }); }); diff --git a/app/livechat/server/config.js b/app/livechat/server/config.js index c3d85b5afed3..2f11b4b24606 100644 --- a/app/livechat/server/config.js +++ b/app/livechat/server/config.js @@ -272,6 +272,14 @@ Meteor.startup(function() { i18nLabel: 'Office_hours_enabled', }); + settings.add('Livechat_allow_online_agents_outside_office_hours', true, { + type: 'boolean', + group: 'Livechat', + public: true, + i18nLabel: 'Allow_Online_Agents_Outside_Office_Hours', + enableQuery: { _id: 'Livechat_enable_office_hours', value: true }, + }); + settings.add('Livechat_continuous_sound_notification_new_livechat_room', false, { type: 'boolean', group: 'Livechat', diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index 2119a53f9084..6b487baeb71b 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -14,7 +14,18 @@ import { QueueMethods } from './QueueMethods'; import { Analytics } from './Analytics'; import { settings } from '../../../settings'; import { callbacks } from '../../../callbacks'; -import { Users, Rooms, Messages, Subscriptions, Settings, LivechatDepartmentAgents, LivechatDepartment, LivechatCustomField, LivechatVisitors } from '../../../models'; +import { + Users, + Rooms, + Messages, + Subscriptions, + Settings, + LivechatDepartmentAgents, + LivechatDepartment, + LivechatCustomField, + LivechatVisitors, + LivechatOfficeHour, +} from '../../../models'; import { Logger } from '../../../logger'; import { sendMessage, deleteMessage, updateMessage } from '../../../lib'; import { addUserRoles, removeUserFromRoles } from '../../../authorization'; @@ -928,6 +939,22 @@ export const Livechat = { }); }); }, + + allowAgentChangeServiceStatus(statusLivechat) { + if (!settings.get('Livechat_enable_office_hours')) { + return true; + } + + if (settings.get('Livechat_allow_online_agents_outside_office_hours')) { + return true; + } + + if (statusLivechat !== 'available') { + return true; + } + + return LivechatOfficeHour.isNowWithinHours(); + }, }; Livechat.stream = new Meteor.Streamer('livechat-room'); diff --git a/app/livechat/server/methods/changeLivechatStatus.js b/app/livechat/server/methods/changeLivechatStatus.js index e61250830360..70fd73e2ff4a 100644 --- a/app/livechat/server/methods/changeLivechatStatus.js +++ b/app/livechat/server/methods/changeLivechatStatus.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Users } from '../../../models'; +import { Livechat } from '../lib/Livechat'; Meteor.methods({ 'livechat:changeLivechatStatus'() { @@ -11,6 +12,9 @@ Meteor.methods({ const user = Meteor.user(); const newStatus = user.statusLivechat === 'available' ? 'not-available' : 'available'; + if (!Livechat.allowAgentChangeServiceStatus(newStatus)) { + throw new Meteor.Error('error-office-hours-are-closed', 'Not allowed', { method: 'livechat:changeLivechatStatus' }); + } return Users.setLivechatStatus(user._id, newStatus); }, diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 43b9d366c7ab..b9a8ac3c7461 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -255,6 +255,7 @@ "Allow_Invalid_SelfSigned_Certs_Description": "Allow invalid and self-signed SSL certificate's for link validation and previews.", "Allow_switching_departments": "Allow Visitor to Switch Departments", "Allow_Marketing_Emails": "Allow Marketing Emails", + "Allow_Online_Agents_Outside_Office_Hours" : "Allow online agents outside of office hours", "Almost_done": "Almost done", "Alphabetical": "Alphabetical", "Always_open_in_new_window": "Always Open in New Window", @@ -1266,6 +1267,7 @@ "error-no-tokens-for-this-user": "There are no tokens for this user", "error-not-allowed": "Not allowed", "error-not-authorized": "Not authorized", + "error-office-hours-are-closed": "The office hours are closed.", "error-password-policy-not-met": "Password does not meet the server's policy", "error-password-policy-not-met-maxLength": "Password does not meet the server's policy of maximum length (password too long)", "error-password-policy-not-met-minLength": "Password does not meet the server's policy of minimum length (password too short)", diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 4c8164e5db32..1ec820548731 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -251,6 +251,7 @@ "Allow_Invalid_SelfSigned_Certs_Description": "Permitir certificado SSL inválidos e auto-assinados para validação de link e previews.", "Allow_switching_departments": "Permitir que Visitantes Mudem de Departamento", "Allow_Marketing_Emails": "Permitir emails de marketing", + "Allow_Online_Agents_Outside_Office_Hours" : "Permitir agentes online fora do horário de escritório", "Almost_done": "Quase pronto", "Alphabetical": "Alfabética", "Always_open_in_new_window": "Sempra Abrir em Janela Nova", @@ -1227,6 +1228,7 @@ "error-no-tokens-for-this-user": "Não existem tokens para este usuário", "error-not-allowed": "Não permitido", "error-not-authorized": "Não autorizado", + "error-office-hours-are-closed": "O horário de escritório esta fechado.", "error-password-policy-not-met": "A senha não atende a política do servidor", "error-password-policy-not-met-maxLength": "A senha não está de acordo com a política de comprimento máximo do servidor (senha muito longa)", "error-password-policy-not-met-minLength": "A senha não está de acordo com a política de comprimento mínimo do servidor (senha muito curta)", From edea473b8c32b3857aa6f75a3ef7c31aff9abb00 Mon Sep 17 00:00:00 2001 From: "Pierre H. Lehnen" Date: Thu, 11 Jul 2019 08:23:28 -0300 Subject: [PATCH 12/87] [FIX] Custom status displayed on room leader panel (#14958) --- app/ui/client/views/app/room.html | 2 +- app/ui/client/views/app/room.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ui/client/views/app/room.html b/app/ui/client/views/app/room.html index a676a96d5356..9e1868987e6b 100644 --- a/app/ui/client/views/app/room.html +++ b/app/ui/client/views/app/room.html @@ -87,7 +87,7 @@
{{name}}
- {{_ statusDisplay}} + {{statusDisplay}}
{{_ "Chat_Now"}} diff --git a/app/ui/client/views/app/room.js b/app/ui/client/views/app/room.js index b72edd3c07d4..a067a82f13c7 100644 --- a/app/ui/client/views/app/room.js +++ b/app/ui/client/views/app/room.js @@ -321,7 +321,7 @@ Template.room.helpers({ ...roles.u, name: settings.get('UI_Use_Real_Name') ? roles.u.name || roles.u.username : roles.u.username, status: leader.status || 'offline', - statusDisplay: leader.statusText || leader.status || 'offline', + statusDisplay: leader.statusText || t(leader.status || 'offline'), }; } }, From 7762aaa26732227106fe783d71088cb8db77ce76 Mon Sep 17 00:00:00 2001 From: magicbelette Date: Thu, 11 Jul 2019 13:38:23 +0200 Subject: [PATCH 13/87] [FIX] LDAP login with customField sync (#14808) Closes #14661 --- app/ldap/server/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ldap/server/sync.js b/app/ldap/server/sync.js index 550c079a697f..ac9d6fe33f0b 100644 --- a/app/ldap/server/sync.js +++ b/app/ldap/server/sync.js @@ -144,7 +144,7 @@ export function getDataToSyncUserData(ldapUser, user) { if (currKey === lastKey) { obj[currKey] = tmpLdapField; } else { - obj[currKey] = obj[currKey]; + obj[currKey] = obj[currKey] || {}; } return obj[currKey]; }, userData); From d0763c6625f08ce754f97bb0b0ec0fa8707577a5 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Thu, 11 Jul 2019 10:52:34 -0300 Subject: [PATCH 14/87] [FIX] Prevent error on trying insert message with duplicated id (#14945) --- app/lib/client/methods/sendMessage.js | 6 ++++++ app/lib/server/functions/sendMessage.js | 4 ++++ packages/rocketchat-i18n/i18n/en.i18n.json | 1 + 3 files changed, 11 insertions(+) diff --git a/app/lib/client/methods/sendMessage.js b/app/lib/client/methods/sendMessage.js index 4ba64c0059a7..d765eefe27c7 100644 --- a/app/lib/client/methods/sendMessage.js +++ b/app/lib/client/methods/sendMessage.js @@ -1,17 +1,23 @@ import { Meteor } from 'meteor/meteor'; import { TimeSync } from 'meteor/mizzao:timesync'; import s from 'underscore.string'; +import toastr from 'toastr'; import { ChatMessage } from '../../../models'; import { settings } from '../../../settings'; import { callbacks } from '../../../callbacks'; import { promises } from '../../../promises/client'; +import { t } from '../../../utils/client'; Meteor.methods({ sendMessage(message) { if (!Meteor.userId() || s.trim(message.msg) === '') { return false; } + const messageAlreadyExists = message._id && ChatMessage.findOne({ _id: message._id }); + if (messageAlreadyExists) { + return toastr.error(t('Message_Already_Sent')); + } const user = Meteor.user(); message.ts = isNaN(TimeSync.serverOffset()) ? new Date() : new Date(Date.now() + TimeSync.serverOffset()); message.u = { diff --git a/app/lib/server/functions/sendMessage.js b/app/lib/server/functions/sendMessage.js index 290218f14e3d..2cb04ff465cb 100644 --- a/app/lib/server/functions/sendMessage.js +++ b/app/lib/server/functions/sendMessage.js @@ -198,6 +198,10 @@ export const sendMessage = function(user, message, room, upsert = false) { }, message); message._id = _id; } else { + const messageAlreadyExists = message._id && Messages.findOneById(message._id, { fields: { _id: 1 } }); + if (messageAlreadyExists) { + return; + } message._id = Messages.insert(message); } diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index b9a8ac3c7461..b5955587779b 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2010,6 +2010,7 @@ "Message_AllowSnippeting": "Allow Message Snippeting", "Message_AllowStarring": "Allow Message Starring", "Message_AllowUnrecognizedSlashCommand": "Allow Unrecognized Slash Commands", + "Message_Already_Sent": "This message has already been sent and is being processed by the server", "Message_AlwaysSearchRegExp": "Always Search Using RegExp", "Message_AlwaysSearchRegExp_Description": "We recommend to set `True` if your language is not supported on MongoDB text search.", "Message_Attachments": "Message Attachments", From 0e023ee1d7c2ef612b0454df8e704c62082f7e58 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 11 Jul 2019 11:27:29 -0300 Subject: [PATCH 15/87] [FIX] OTR key icon missing on messages (#14953) --- app/ui-message/client/message.html | 1 + app/ui-message/client/message.js | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/ui-message/client/message.html b/app/ui-message/client/message.html index a155f1697046..5e80a3329fce 100644 --- a/app/ui-message/client/message.html +++ b/app/ui-message/client/message.html @@ -27,6 +27,7 @@
+ {{#each role in roleTags}} {{role.description}} {{/each}} diff --git a/app/ui-message/client/message.js b/app/ui-message/client/message.js index b0fcd419455a..97d400f6b2fb 100644 --- a/app/ui-message/client/message.js +++ b/app/ui-message/client/message.js @@ -204,6 +204,10 @@ Template.message.helpers({ return 'own'; } }, + t() { + const { msg } = this; + return msg.t; + }, timestamp() { const { msg } = this; return +msg.ts; From ea1c99f2e3cc2467ca6a4332b7a71d0f1567e0f5 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Thu, 11 Jul 2019 12:03:34 -0300 Subject: [PATCH 16/87] [FIX] Method `getUsersOfRoom` not returning offline users if limit is not defined (#14753) --- server/methods/getUsersOfRoom.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server/methods/getUsersOfRoom.js b/server/methods/getUsersOfRoom.js index dda28c79a08f..951e76091e88 100644 --- a/server/methods/getUsersOfRoom.js +++ b/server/methods/getUsersOfRoom.js @@ -60,13 +60,12 @@ Meteor.methods({ const total = Subscriptions.findByRoomIdWhenUsernameExists(rid).count(); const users = await findUsers({ rid, status: { $ne: 'offline' }, limit, skip }); - - if (showAll && users.length < limit) { + if (showAll && (!limit || users.length < limit)) { const offlineUsers = await findUsers({ rid, status: { $eq: 'offline' }, - limit: limit - users.length, - skip, + limit: limit ? limit - users.length : 0, + skip: skip || 0, }); return { From 8397e2db79d437f58eb11011bed9692f6a0f289b Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 11 Jul 2019 14:31:37 -0300 Subject: [PATCH 17/87] [IMPROVE] Remove too specific helpers isFirefox() and isChrome() (#14963) --- app/ui/client/components/icon.js | 17 +++++++++++++++-- app/ui/client/index.js | 1 - app/utils/client/index.js | 1 - app/utils/client/lib/browsers.js | 2 -- package-lock.json | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) delete mode 100644 app/utils/client/lib/browsers.js diff --git a/app/ui/client/components/icon.js b/app/ui/client/components/icon.js index 779ea70f1632..6e759b01b17c 100644 --- a/app/ui/client/components/icon.js +++ b/app/ui/client/components/icon.js @@ -1,10 +1,23 @@ import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; -import { isChrome, isFirefox } from '../../../utils'; +import './icon.html'; + const baseUrlFix = () => `${ document.baseURI }${ FlowRouter.current().path.substring(1) }`; +const isMozillaFirefoxBelowVersion = (upperVersion) => { + const [, version] = navigator.userAgent.match(/Firefox\/(\d+)\.\d/) || []; + return parseInt(version, 10) < upperVersion; +}; + +const isGoogleChromeBelowVersion = (upperVersion) => { + const [, version] = navigator.userAgent.match(/Chrome\/(\d+)\.\d/) || []; + return parseInt(version, 10) < upperVersion; +}; + +const isBaseUrlFixNeeded = () => isMozillaFirefoxBelowVersion(55) || isGoogleChromeBelowVersion(55); + Template.icon.helpers({ - baseUrl: (isFirefox && isFirefox[1] < 55) || (isChrome && isChrome[1] < 55) ? baseUrlFix : undefined, + baseUrl: isBaseUrlFixNeeded() ? baseUrlFix : undefined, }); diff --git a/app/ui/client/index.js b/app/ui/client/index.js index aafc4dc9cbfd..7e4197768dd8 100644 --- a/app/ui/client/index.js +++ b/app/ui/client/index.js @@ -44,7 +44,6 @@ import './views/app/secretURL'; import './views/app/videoCall/videoButtons'; import './views/app/videoCall/videoCall'; import './views/app/photoswipe'; -import './components/icon.html'; import './components/icon'; import './components/table.html'; import './components/table'; diff --git a/app/utils/client/index.js b/app/utils/client/index.js index 5327658971bb..35e0793f642d 100644 --- a/app/utils/client/index.js +++ b/app/utils/client/index.js @@ -1,5 +1,4 @@ export { t, isRtl } from '../lib/tapi18n'; -export { isChrome, isFirefox } from './lib/browsers'; export { getDefaultSubscriptionPref } from '../lib/getDefaultSubscriptionPref'; export { Info } from '../rocketchat.info'; export { isEmail } from '../lib/isEmail'; diff --git a/app/utils/client/lib/browsers.js b/app/utils/client/lib/browsers.js deleted file mode 100644 index 986d3f59d395..000000000000 --- a/app/utils/client/lib/browsers.js +++ /dev/null @@ -1,2 +0,0 @@ -export const isFirefox = navigator.userAgent.match(/Firefox\/(\d+)\.\d/); -export const isChrome = navigator.userAgent.match(/Chrome\/(\d+)\.\d/); diff --git a/package-lock.json b/package-lock.json index d39f8545b291..f85d093451d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "Rocket.Chat", - "version": "1.2.0-develop", + "version": "1.3.0-develop", "lockfileVersion": 1, "requires": true, "dependencies": { From ef62371faf752f294660176747d337518453f650 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 11 Jul 2019 14:38:11 -0300 Subject: [PATCH 18/87] [FIX] Jump to message missing in Starred Messages (#14949) --- app/message-pin/client/actionButton.js | 2 +- app/message-star/client/actionButton.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/message-pin/client/actionButton.js b/app/message-pin/client/actionButton.js index 1fcf1f19cc21..7ea7f5913f4e 100644 --- a/app/message-pin/client/actionButton.js +++ b/app/message-pin/client/actionButton.js @@ -64,7 +64,7 @@ Meteor.startup(function() { id: 'jump-to-pin-message', icon: 'jump', label: 'Jump_to_message', - context: ['pinned'], + context: ['pinned', 'message', 'message-mobile'], action() { const { msg: message } = messageArgs(this); if (window.matchMedia('(max-width: 500px)').matches) { diff --git a/app/message-star/client/actionButton.js b/app/message-star/client/actionButton.js index fee7c64c2dba..c6898ef9d50f 100644 --- a/app/message-star/client/actionButton.js +++ b/app/message-star/client/actionButton.js @@ -63,7 +63,7 @@ Meteor.startup(function() { id: 'jump-to-star-message', icon: 'jump', label: 'Jump_to_message', - context: ['starred', 'threads'], + context: ['starred', 'threads', 'message', 'message-mobile'], action() { const { msg: message } = messageArgs(this); if (window.matchMedia('(max-width: 500px)').matches) { From 564d5d5385c7e7431d5799b65ecd48deb9f899df Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 11 Jul 2019 14:39:32 -0300 Subject: [PATCH 19/87] [IMPROVE] Update tabs markup (#14964) --- app/theme/client/imports/components/tabs.css | 17 +++++++++++++++-- app/ui/client/components/tabs.html | 13 ++++++++++--- app/ui/client/components/tabs.js | 16 +++++++++++----- app/ui/client/index.js | 1 - 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/app/theme/client/imports/components/tabs.css b/app/theme/client/imports/components/tabs.css index cf7a87448cd2..187394482509 100644 --- a/app/theme/client/imports/components/tabs.css +++ b/app/theme/client/imports/components/tabs.css @@ -13,8 +13,9 @@ } .tab { + display: flex; + margin: 0 1rem; - padding: 1rem 0; cursor: pointer; @@ -24,13 +25,25 @@ border-bottom: 2px solid transparent; + font-family: inherit; font-size: 1rem; - font-weight: 500; line-height: 1.25rem; + align-items: stretch; + flex-flow: row nowrap; &.active { color: var(--rc-color-button-primary); border-bottom-color: var(--rc-color-button-primary); } + + &:focus { + text-decoration: underline; + } + + & > span { + flex: 1; + + padding: 1rem 0; + } } diff --git a/app/ui/client/components/tabs.html b/app/ui/client/components/tabs.html index 37ec16ef2270..5ed7ec7395d5 100644 --- a/app/ui/client/components/tabs.html +++ b/app/ui/client/components/tabs.html @@ -1,8 +1,15 @@ diff --git a/app/ui-flextab/client/tabs/uploadedFilesList.js b/app/ui-flextab/client/tabs/uploadedFilesList.js index 9f65c18d2ad9..abbacb4556db 100644 --- a/app/ui-flextab/client/tabs/uploadedFilesList.js +++ b/app/ui-flextab/client/tabs/uploadedFilesList.js @@ -3,6 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Template } from 'meteor/templating'; import { Mongo } from 'meteor/mongo'; import { ReactiveVar } from 'meteor/reactive-var'; +import { ReactiveDict } from 'meteor/reactive-dict'; import { DateFormat } from '../../../lib/client'; import { canDeleteMessage, getURL, handleError, t } from '../../../utils/client'; @@ -10,18 +11,20 @@ import { popover, modal } from '../../../ui-utils/client'; const roomFiles = new Mongo.Collection('room_files'); +const LIST_SIZE = 50; + Template.uploadedFilesList.onCreated(function() { const { rid } = Template.currentData(); this.searchText = new ReactiveVar(null); - this.hasMore = new ReactiveVar(true); - this.limit = new ReactiveVar(50); + + this.state = new ReactiveDict({ + limit: LIST_SIZE, + hasMore: true, + }); this.autorun(() => { - this.subscribe('roomFilesWithSearchText', rid, this.searchText.get(), this.limit.get(), () => { - if (roomFiles.find({ rid }).fetch().length < this.limit.get()) { - this.hasMore.set(false); - } - }); + const ready = this.subscribe('roomFilesWithSearchText', rid, this.searchText.get(), this.state.get('limit'), () => this.state.set('hasMore', this.state.get('limit') <= roomFiles.find({ rid }).count())).ready(); + this.state.set('loading', !ready); }); }); @@ -46,6 +49,9 @@ Template.uploadedFilesList.helpers({ return getURL(this.url); } }, + limit() { + return Template.instance().state.get('limit'); + }, format(timestamp) { return DateFormat.formatDateAndTime(timestamp); }, @@ -96,12 +102,8 @@ Template.uploadedFilesList.helpers({ return DateFormat.formatDateAndTime(timestamp); }, - hasMore() { - return Template.instance().hasMore.get(); - }, - - hasFiles() { - return roomFiles.find({ rid: this.rid }).count() > 0; + isLoading() { + return Template.instance().state.get('loading'); }, }); @@ -112,12 +114,15 @@ Template.uploadedFilesList.events({ 'input .uploaded-files-list__search-input'(e, t) { t.searchText.set(e.target.value.trim()); - t.hasMore.set(true); + t.state.set('hasMore', true); }, 'scroll .flex-tab__result': _.throttle(function(e, t) { if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight)) { - return t.limit.set(t.limit.get() + 50); + if (!t.state.get('hasMore')) { + return; + } + return t.state.set('limit', t.state.get('limit') + LIST_SIZE); } }, 200), From d5b1de5d553fe485ab31d0d3be19665d9af9814e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 11 Jul 2019 15:44:06 -0300 Subject: [PATCH 23/87] [FIX] 50 custom emoji limit (#14951) --- app/emoji-custom/client/admin/adminEmoji.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/emoji-custom/client/admin/adminEmoji.js b/app/emoji-custom/client/admin/adminEmoji.js index 3d64e3b189ce..7a46f4c9cb0c 100644 --- a/app/emoji-custom/client/admin/adminEmoji.js +++ b/app/emoji-custom/client/admin/adminEmoji.js @@ -42,6 +42,18 @@ Template.adminEmoji.helpers({ data: Template.instance().tabBarData.get(), }; }, + onTableScroll() { + const instance = Template.instance(); + return function(currentTarget) { + if ((currentTarget.offsetHeight + currentTarget.scrollTop) < (currentTarget.scrollHeight - 100)) { + return; + } + if (Template.instance().limit.get() > Template.instance().customemoji().length) { + return false; + } + instance.limit.set(instance.limit.get() + 50); + }; + }, onTableItemClick() { const instance = Template.instance(); return function({ _id }) { From 7f887803d7f206bea0b61890179216bc5fb64f3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jul 2019 15:47:22 -0300 Subject: [PATCH 24/87] =?UTF-8?q?Bump=20jquery=20from=203.3.1=20to=203.4.0?= =?UTF-8?q?=20in=20/packages/rocketchat-livech=E2=80=A6=20(#14922)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [jquery](https://github.com/jquery/jquery) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/jquery/jquery/releases) - [Commits](https://github.com/jquery/jquery/compare/3.3.1...3.4.0) Signed-off-by: dependabot[bot] --- packages/rocketchat-livechat/.app/package-lock.json | 6 +++--- packages/rocketchat-livechat/.app/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rocketchat-livechat/.app/package-lock.json b/packages/rocketchat-livechat/.app/package-lock.json index db363ce6d511..ffaf140c2898 100644 --- a/packages/rocketchat-livechat/.app/package-lock.json +++ b/packages/rocketchat-livechat/.app/package-lock.json @@ -490,9 +490,9 @@ } }, "jquery": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", - "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.0.tgz", + "integrity": "sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==" }, "media-typer": { "version": "0.3.0", diff --git a/packages/rocketchat-livechat/.app/package.json b/packages/rocketchat-livechat/.app/package.json index acca48afe6c1..d28deaf472b3 100644 --- a/packages/rocketchat-livechat/.app/package.json +++ b/packages/rocketchat-livechat/.app/package.json @@ -24,7 +24,7 @@ "autolinker": "^1.8.1", "bcrypt": "^3.0.2", "core-js": "^2.5.7", - "jquery": "^3.3.1", + "jquery": "^3.4.0", "meteor-node-stubs": "^0.4.1", "mime-db": "^1.37.0", "mime-type": "^3.0.7", From f63c9e6986e64cd4045ce79906d49287778f382e Mon Sep 17 00:00:00 2001 From: Renato Becker Date: Thu, 11 Jul 2019 17:56:59 -0300 Subject: [PATCH 25/87] [FIX] Allow storing the navigation history of unregistered Livechat visitors (#14970) * Remove validations before storing visitor navigation history. * Removed unused imports. --- app/livechat/server/api/v1/pageVisited.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/livechat/server/api/v1/pageVisited.js b/app/livechat/server/api/v1/pageVisited.js index 4b35cd3adb6e..e5ef7c42ba64 100644 --- a/app/livechat/server/api/v1/pageVisited.js +++ b/app/livechat/server/api/v1/pageVisited.js @@ -1,9 +1,7 @@ -import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; import { API } from '../../../../api'; -import { findGuest, findRoom } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; API.v1.addRoute('livechat/page.visited', { @@ -11,7 +9,7 @@ API.v1.addRoute('livechat/page.visited', { try { check(this.bodyParams, { token: String, - rid: String, + rid: Match.Maybe(String), pageInfo: Match.ObjectIncluding({ change: String, title: String, @@ -22,17 +20,6 @@ API.v1.addRoute('livechat/page.visited', { }); const { token, rid, pageInfo } = this.bodyParams; - - const guest = findGuest(token); - if (!guest) { - throw new Meteor.Error('invalid-token'); - } - - const room = findRoom(token, rid); - if (!room) { - throw new Meteor.Error('invalid-room'); - } - const obj = Livechat.savePageHistory(token, rid, pageInfo); if (obj) { const page = _.pick(obj, 'msg', 'navigation'); From 98e5c92b27836ee50532c96b50d72d9c8adb5049 Mon Sep 17 00:00:00 2001 From: Gabriel Engel Date: Thu, 11 Jul 2019 17:57:07 -0300 Subject: [PATCH 26/87] Update GPG key --- .circleci/sign.key.gpg | Bin 5117 -> 21432 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/.circleci/sign.key.gpg b/.circleci/sign.key.gpg index 488e275998d505474fd6a93c263b4931885d7d00..6d005764c11b23a4f32ab65cd8d3961dd8fc3899 100644 GIT binary patch literal 21432 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 Date: Thu, 11 Jul 2019 18:12:52 -0300 Subject: [PATCH 27/87] [FIX] Wrong label order on room settings (#14960) --- app/channel-settings/client/views/channelSettings.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/channel-settings/client/views/channelSettings.html b/app/channel-settings/client/views/channelSettings.html index 7e736cbeee8c..32f93b538b39 100644 --- a/app/channel-settings/client/views/channelSettings.html +++ b/app/channel-settings/client/views/channelSettings.html @@ -122,9 +122,9 @@