From eed869ac9031c63f47c0ecea7d8ddfa9aa8440c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Fri, 17 Nov 2017 18:28:14 +0100 Subject: [PATCH 01/39] Allow maintenance of per-setting permissions - first draft, not beautified yet --- package-lock.json | 12 +- package.json | 4 +- .../client/lib/SettingPermissions.js | 8 + .../client/views/permissions.html | 66 ++++--- .../client/views/permissions.js | 75 +++++--- .../lib/rocketchat.js | 4 + packages/rocketchat-authorization/package.js | 1 + .../server/publications/permissions.js | 21 ++- .../server/startup.js | 174 +++++++++++------- .../server/publications/settings.js | 30 +++ .../rocketchat-livechat/app/package-lock.json | 22 +-- packages/rocketchat-ui-admin/client/admin.js | 82 ++++++--- 12 files changed, 337 insertions(+), 162 deletions(-) create mode 100644 packages/rocketchat-authorization/client/lib/SettingPermissions.js diff --git a/package-lock.json b/package-lock.json index 9827c6fdf1d0..139e1f2264cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -327,7 +327,7 @@ "async": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=", "requires": { "lodash": "4.17.4" } @@ -352,7 +352,7 @@ "autoprefixer": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.6.tgz", - "integrity": "sha512-C9yv/UF3X+eJTi/zvfxuyfxmLibYrntpF3qoJYrMeQwgUJOZrZvpJiMG2FMQ3qnhWtF/be4pYONBBw95ZGe3vA==", + "integrity": "sha1-+5MwOfdK90qD5xIlznjZ/Vi6hNc=", "dev": true, "requires": { "browserslist": "2.6.1", @@ -6927,7 +6927,7 @@ "postcss-custom-properties": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-6.2.0.tgz", - "integrity": "sha512-eNR2h9T9ciKMoQEORrPjH33XeN/nuvVuxArOKmHtsFbGbNss631tgTrKou3/pmjAZbA4QQkhLIkPQkIk3WW+8w==", + "integrity": "sha1-XZKafwbpuE4PETNBlMC6mjCs++k=", "dev": true, "requires": { "balanced-match": "1.0.0", @@ -6949,7 +6949,7 @@ "postcss-less": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.1.tgz", - "integrity": "sha512-zl0EEqq8Urh37Ppdv9zzhpZpLHrgkxmt6e3O4ftRa7/b8Uq2LV+/KBVM8/KuzmHNu+mthhOArg1lxbfqQ3NUdg==", + "integrity": "sha1-S9JA21F840B1g9knhYGE9QBF9Ks=", "dev": true, "requires": { "postcss": "5.2.18" @@ -7035,7 +7035,7 @@ "postcss-nesting": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-3.0.0.tgz", - "integrity": "sha512-ijQlEXUcYXXNPdLMFcMEr4i5SEPRKR8fq/Iya4L0mQbNOCz+szTGCBlf0Cvu2HiQLjCNqLnGO4fKFLbNnXe7Ag==", + "integrity": "sha1-sKdJ1+/xHSVQoE3qZCtpXXWC+x0=", "dev": true, "requires": { "postcss": "6.0.13" @@ -7562,7 +7562,7 @@ "retry-request": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-3.1.0.tgz", - "integrity": "sha512-jOwZQlWR/boHhbAfzfOoUn28EDDotW2A7YxV2o5mfBb07H0k/zZAgbxRcckW08GKl/aT0JtPk1NViuk2BfHqVg==", + "integrity": "sha1-+UjKF5JlemSryJoPrXJ90HtisNY=", "requires": { "request": "2.83.0", "through2": "2.0.3" diff --git a/package.json b/package.json index 06fc561dba30..8b92ce12e480 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,9 @@ "chat" ], "scripts": { - "start": "meteor npm i && meteor", + "start": "meteor npm i && meteor run", + "debug": "meteor run --inspect", + "debug-brk": "meteor run --inspect-brk", "lint": "eslint .", "lint-fix": "eslint . --fix", "stylelint": "stylelint packages/**/*.css", diff --git a/packages/rocketchat-authorization/client/lib/SettingPermissions.js b/packages/rocketchat-authorization/client/lib/SettingPermissions.js new file mode 100644 index 000000000000..1ae099a28c64 --- /dev/null +++ b/packages/rocketchat-authorization/client/lib/SettingPermissions.js @@ -0,0 +1,8 @@ +RocketChat.authz.settingCachedCollectiong = new RocketChat.CachedCollection({ + name: 'setting-permissions', + eventType: 'onLogged', + userRelated: false +}); +RocketChat.authz.settingCachedCollectiong.init(); + +this.SettingPermissions = RocketChat.authz.settingCachedCollectiong.collection; diff --git a/packages/rocketchat-authorization/client/views/permissions.html b/packages/rocketchat-authorization/client/views/permissions.html index e0e47d6b426d..8f88f6e3948d 100644 --- a/packages/rocketchat-authorization/client/views/permissions.html +++ b/packages/rocketchat-authorization/client/views/permissions.html @@ -1,34 +1,46 @@ + diff --git a/packages/rocketchat-authorization/client/views/permissions.js b/packages/rocketchat-authorization/client/views/permissions.js index a7ef073d520e..49ed36dfac48 100644 --- a/packages/rocketchat-authorization/client/views/permissions.js +++ b/packages/rocketchat-authorization/client/views/permissions.js @@ -1,4 +1,6 @@ /* globals ChatPermissions, SettingPermissions */ +import {permissionLevel} from '../../lib/rocketchat'; + Template.permissions.helpers({ roles() { return Template.instance().roles.get(); @@ -54,7 +56,7 @@ Template.permissionsTable.helpers({ }, permissionName(permission) { - return t(permission._id); + return permission.level === permissionLevel.SETTING ? t(permission.settingId) : t(permission._id); }, permissionDescription(permission) { diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 958f7c5b8217..db32471cc2ce 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -113,7 +113,8 @@ Meteor.startup(function() { const permissionId = getSettingPermissionId(setting._id); const permission = { _id: permissionId, - level: permissionLevel.SETTING + level: permissionLevel.SETTING, + settingId: setting._id }; // copy previously assigned roles if available if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { diff --git a/packages/rocketchat-ui-admin/client/admin.html b/packages/rocketchat-ui-admin/client/admin.html index b47e6e83cf5f..a63acd6c93c6 100644 --- a/packages/rocketchat-ui-admin/client/admin.html +++ b/packages/rocketchat-ui-admin/client/admin.html @@ -23,7 +23,7 @@

{{description}}

{{/if}} -
+
{{#each sections}}
{{#if section}} From c87a30d3afc0f03553bcf6183d7ad68fe3287d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Sun, 19 Nov 2017 20:20:05 +0100 Subject: [PATCH 04/39] Add path to permission title --- .../client/views/permissions.js | 14 +++++++++++++- .../server/publications/permissions.js | 2 +- .../rocketchat-authorization/server/startup.js | 4 +++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/rocketchat-authorization/client/views/permissions.js b/packages/rocketchat-authorization/client/views/permissions.js index 49ed36dfac48..bd33ff69ec5c 100644 --- a/packages/rocketchat-authorization/client/views/permissions.js +++ b/packages/rocketchat-authorization/client/views/permissions.js @@ -56,7 +56,19 @@ Template.permissionsTable.helpers({ }, permissionName(permission) { - return permission.level === permissionLevel.SETTING ? t(permission.settingId) : t(permission._id); + if (permission.level === permissionLevel.SETTING) { + let path = ''; + if (permission.group) { + path = `${ t(permission.group) } > `; + } + if (permission.section) { + path = `${ path }${ t(permission.section) } > `; + } + path = `${ path }${ t(permission.settingId) }`; + return path; + } else { + return t(permission._id); + } }, permissionDescription(permission) { diff --git a/packages/rocketchat-authorization/server/publications/permissions.js b/packages/rocketchat-authorization/server/publications/permissions.js index a18e3faff476..1c967cfa7619 100644 --- a/packages/rocketchat-authorization/server/publications/permissions.js +++ b/packages/rocketchat-authorization/server/publications/permissions.js @@ -28,7 +28,7 @@ Meteor.methods({ const records = RocketChat.models.Permissions.find({ level: permissionLevel.SETTING, groupPermissionId: {$exists: true} //filter group permissions themselves, as they are being assigned implicitly - }).fetch(); + }, {}, {sort:{group: 1, section: 1}}).fetch(); if (updatedAt instanceof Date) { return { diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index db32471cc2ce..23ad9738708c 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -114,7 +114,9 @@ Meteor.startup(function() { const permission = { _id: permissionId, level: permissionLevel.SETTING, - settingId: setting._id + settingId: setting._id, + group: setting.group, + section: setting.section }; // copy previously assigned roles if available if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { From 48b107602d02e5f81c2189b068f9c7cba594c90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Mon, 20 Nov 2017 08:22:50 +0100 Subject: [PATCH 05/39] Permission to access setting permissions --- .../client/views/permissions.html | 44 ++++++++++--------- .../client/views/permissions.js | 4 ++ .../server/methods/addPermissionToRole.js | 7 ++- .../methods/removeRoleFromPermission.js | 8 +++- .../server/publications/permissions.js | 2 +- .../server/startup.js | 1 + packages/rocketchat-i18n/i18n/de.i18n.json | 2 + packages/rocketchat-i18n/i18n/en.i18n.json | 2 + 8 files changed, 47 insertions(+), 23 deletions(-) diff --git a/packages/rocketchat-authorization/client/views/permissions.html b/packages/rocketchat-authorization/client/views/permissions.html index b7382df1ac2f..5bdaf31c4328 100644 --- a/packages/rocketchat-authorization/client/views/permissions.html +++ b/packages/rocketchat-authorization/client/views/permissions.html @@ -41,29 +41,33 @@ {{> permissionsTable permissions=permissions allRoles=roles}}
-
-
-
-
- {{_ "setting-permissions"}}
-
- + {{#if hasSettingPermission}} +
+
+
+
+ {{_ "Setting_permissions"}}
+
+ +
+
+
+ {{#if settingPermissionExpanded }} + {{> permissionsTable permissions=settingPermissions allRoles=roles}} + {{else}} + {{_ "Not_authorized"}} + {{/if}}
-
-
- {{#if settingPermissionExpanded }} - {{> permissionsTable permissions=settingPermissions allRoles=roles}} - {{/if}}
-
+ {{/if}} {{else}} {{_ "Not_authorized"}} {{/if}} diff --git a/packages/rocketchat-authorization/client/views/permissions.js b/packages/rocketchat-authorization/client/views/permissions.js index bd33ff69ec5c..143c253d8787 100644 --- a/packages/rocketchat-authorization/client/views/permissions.js +++ b/packages/rocketchat-authorization/client/views/permissions.js @@ -26,6 +26,10 @@ Template.permissions.helpers({ return RocketChat.authz.hasAllPermission('access-permissions'); }, + hasSettingPermission() { + return RocketChat.authz.hasAllPermission('access-setting-permissions'); + }, + settingPermissionExpanded() { return Template.instance().settingPermissionsExpanded.get(); } diff --git a/packages/rocketchat-authorization/server/methods/addPermissionToRole.js b/packages/rocketchat-authorization/server/methods/addPermissionToRole.js index 9fe4a94f2144..e2d2e85c2a9b 100644 --- a/packages/rocketchat-authorization/server/methods/addPermissionToRole.js +++ b/packages/rocketchat-authorization/server/methods/addPermissionToRole.js @@ -1,12 +1,17 @@ +import {permissionLevel} from '../../lib/rocketchat'; + Meteor.methods({ 'authorization:addPermissionToRole'(permission, role) { - if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions')) { + if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions') + || (permission.level === permissionLevel.SETTING && !RocketChat.authz.hasPermission(Meteor.userId(), 'access-setting-permissions')) + ) { throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', { method: 'authorization:addPermissionToRole', action: 'Adding_permission' }); } + // for setting-based-permissions, authorize the group access as well const addParentPermissions = function(permissionId, role) { const permission = RocketChat.models.Permissions.findOneById(permissionId); if (permission.groupPermissionId) { diff --git a/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js b/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js index b48f9752d9f4..d9c828bba5fb 100644 --- a/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js +++ b/packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js @@ -1,12 +1,18 @@ +import {permissionLevel} from '../../lib/rocketchat'; + Meteor.methods({ 'authorization:removeRoleFromPermission'(permission, role) { - if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions')) { + if (!Meteor.userId() || !RocketChat.authz.hasPermission(Meteor.userId(), 'access-permissions') + || (permission.level === permissionLevel.SETTING && !RocketChat.authz.hasPermission(Meteor.userId(), 'access-setting-permissions')) + ) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:removeRoleFromPermission', action: 'Accessing_permissions' }); } + // for setting based permissions, revoke the group permission once all setting permissions + // related to this group have been removed const removeStaleParentPermissions = function(permissionId, role) { const permission = RocketChat.models.Permissions.findOneById(permissionId); if (permission.groupPermissionId) { diff --git a/packages/rocketchat-authorization/server/publications/permissions.js b/packages/rocketchat-authorization/server/publications/permissions.js index 1c967cfa7619..5963b532a048 100644 --- a/packages/rocketchat-authorization/server/publications/permissions.js +++ b/packages/rocketchat-authorization/server/publications/permissions.js @@ -28,7 +28,7 @@ Meteor.methods({ const records = RocketChat.models.Permissions.find({ level: permissionLevel.SETTING, groupPermissionId: {$exists: true} //filter group permissions themselves, as they are being assigned implicitly - }, {}, {sort:{group: 1, section: 1}}).fetch(); + }, {}, {sort: {group: 1, section: 1}}).fetch(); if (updatedAt instanceof Date) { return { diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index 23ad9738708c..f8e23016b3cd 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -10,6 +10,7 @@ Meteor.startup(function() { // 2. admin, moderator, and user roles should not be deleted as they are referened in the code. const permissions = [ {_id: 'access-permissions', roles: ['admin']}, + {_id: 'access-setting-permissions', roles: ['admin']}, {_id: 'add-oauth-service', roles: ['admin']}, {_id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator']}, {_id: 'add-user-to-any-c-room', roles: ['admin']}, diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index 1d5195f8380d..c3b0830d6c26 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -16,6 +16,7 @@ "access-mailer_description": "Berechtigung, Massen-E-Mails an alle Benutzer zu versenden.", "access-permissions": "Zugriff auf die Berechtigungs-Übersicht", "access-permissions_description": "Anpassen der Berechtigungen für die unterschiedlichen Rollen.", + "access-setting-permissions": "Zugriff die Übersicht der Einstellungs-Berechtigungen", "Access_not_authorized": "Der Zugriff ist nicht gestattet.", "Access_Token_URL": "URL des Access-Token", "Accessing_permissions": "Zugriff auf Berechtigungen", @@ -1573,6 +1574,7 @@ "Set_as_leader": "Zum Diskussionsleiter ernennen", "Set_as_moderator": "Zum Moderator ernennen", "Set_as_owner": "Zum Besitzer machen", + "Setting_permissions": "Berechtigung, Einstellungen zu ändern", "Settings": "Einstellungen", "Settings_updated": "Die Einstellungen wurden aktualisiert", "Share_Location_Title": "Standort teilen?", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 823a77f35cc5..ed9539cbee2e 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -16,6 +16,7 @@ "access-mailer_description": "Permission to send mass email to all users.", "access-permissions": "Access Permissions Screen", "access-permissions_description": "Modify permissions for various roles.", + "access-setting-permissions": "Modify setting-based permissions", "Access_not_authorized": "Access not authorized", "Access_Token_URL": "Access Token URL", "Accessing_permissions": "Accessing permissions", @@ -1606,6 +1607,7 @@ "Set_as_leader": "Set as leader", "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", + "Setting_permissions": "Permission to change settings", "Settings": "Settings", "Settings_updated": "Settings updated", "Share_Location_Title": "Share Location?", From daccad86fb17000346c4c631ea02ef30b352db81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Mon, 20 Nov 2017 08:28:02 +0100 Subject: [PATCH 06/39] Adapt wording --- .../.npm/package/npm-shrinkwrap.json | 6 +++--- packages/rocketchat-i18n/i18n/de.i18n.json | 4 ++-- packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/rocketchat-google-vision/.npm/package/npm-shrinkwrap.json b/packages/rocketchat-google-vision/.npm/package/npm-shrinkwrap.json index 2b45ef4db29d..ff2d82be12c7 100644 --- a/packages/rocketchat-google-vision/.npm/package/npm-shrinkwrap.json +++ b/packages/rocketchat-google-vision/.npm/package/npm-shrinkwrap.json @@ -1124,9 +1124,9 @@ "integrity": "sha1-O0tCACOmbKfjK9uhbnEJN+FNGws=" }, "nan": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=" }, "node-forge": { "version": "0.7.1", diff --git a/packages/rocketchat-i18n/i18n/de.i18n.json b/packages/rocketchat-i18n/i18n/de.i18n.json index c3b0830d6c26..9f4fd1827c4a 100644 --- a/packages/rocketchat-i18n/i18n/de.i18n.json +++ b/packages/rocketchat-i18n/i18n/de.i18n.json @@ -16,7 +16,7 @@ "access-mailer_description": "Berechtigung, Massen-E-Mails an alle Benutzer zu versenden.", "access-permissions": "Zugriff auf die Berechtigungs-Übersicht", "access-permissions_description": "Anpassen der Berechtigungen für die unterschiedlichen Rollen.", - "access-setting-permissions": "Zugriff die Übersicht der Einstellungs-Berechtigungen", + "access-setting-permissions": "Einstellungsbasierte Berechtigungen ändern", "Access_not_authorized": "Der Zugriff ist nicht gestattet.", "Access_Token_URL": "URL des Access-Token", "Accessing_permissions": "Zugriff auf Berechtigungen", @@ -1574,7 +1574,7 @@ "Set_as_leader": "Zum Diskussionsleiter ernennen", "Set_as_moderator": "Zum Moderator ernennen", "Set_as_owner": "Zum Besitzer machen", - "Setting_permissions": "Berechtigung, Einstellungen zu ändern", + "Setting_permissions": "Einstellungsbezogene Berechtigungen", "Settings": "Einstellungen", "Settings_updated": "Die Einstellungen wurden aktualisiert", "Share_Location_Title": "Standort teilen?", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index ed9539cbee2e..dc9812d71c45 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1607,7 +1607,7 @@ "Set_as_leader": "Set as leader", "Set_as_moderator": "Set as moderator", "Set_as_owner": "Set as owner", - "Setting_permissions": "Permission to change settings", + "Setting_permissions": "Setting-based permissions", "Settings": "Settings", "Settings_updated": "Settings updated", "Share_Location_Title": "Share Location?", From 00e4bb545533b056f06eaf72fb4e25bca6f53102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Tue, 21 Nov 2017 16:38:13 +0100 Subject: [PATCH 07/39] UI-adaptation: Allow users with permission 'manage-selected-permissions' to see and change the affected settings. However, this is not reactive: Once the permissions for a particular setting are changed, the user needs to log off and on again before it becomes effective in the UI. This is most probably a consequence of the CachedCollection. This collection needed to be changed on permission-change. In the backend however, the permissions become effective immediately. --- .../client/views/permissions.html | 4 +- .../client/views/permissions.js | 11 +++-- .../server/publications/permissions.js | 3 ++ .../server/startup.js | 2 +- .../server/methods/saveSetting.js | 8 +++- .../server/publications/settings.js | 42 ++++--------------- .../rocketchat-ui-admin/client/admin.html | 2 +- packages/rocketchat-ui-admin/client/admin.js | 20 +++------ .../rocketchat-ui-admin/client/adminFlex.html | 2 +- .../rocketchat-ui-admin/client/adminFlex.js | 4 ++ .../client/accountBox.js | 2 +- 11 files changed, 40 insertions(+), 60 deletions(-) diff --git a/packages/rocketchat-authorization/client/views/permissions.html b/packages/rocketchat-authorization/client/views/permissions.html index 5bdaf31c4328..5b63d667e007 100644 --- a/packages/rocketchat-authorization/client/views/permissions.html +++ b/packages/rocketchat-authorization/client/views/permissions.html @@ -38,7 +38,7 @@ {{_ "New_role"}}
- {{> permissionsTable permissions=permissions allRoles=roles}} + {{> permissionsTable permissions=permissions allRoles=roles collection='Chat'}}
{{#if hasSettingPermission}} @@ -60,7 +60,7 @@
{{#if settingPermissionExpanded }} - {{> permissionsTable permissions=settingPermissions allRoles=roles}} + {{> permissionsTable permissions=settingPermissions allRoles=roles collection='Setting'}} {{else}} {{_ "Not_authorized"}} {{/if}} diff --git a/packages/rocketchat-authorization/client/views/permissions.js b/packages/rocketchat-authorization/client/views/permissions.js index 143c253d8787..4bc4466b920f 100644 --- a/packages/rocketchat-authorization/client/views/permissions.js +++ b/packages/rocketchat-authorization/client/views/permissions.js @@ -94,7 +94,6 @@ Template.permissionsTable.events({ } }); - Template.permissionsTable.onCreated(function() { this.permissionByRole = {}; this.actions = { @@ -114,7 +113,13 @@ Template.permissionsTable.onCreated(function() { delete this.permissionByRole[id]; } }; - ChatPermissions.find().observeChanges(observer); - SettingPermissions.find().observeChanges(observer); + if (this.data.collection === 'Chat') { + ChatPermissions.find().observeChanges(observer); + } + + if (this.data.collection === 'Setting') { + SettingPermissions.find().observeChanges(observer); + } }); }); + diff --git a/packages/rocketchat-authorization/server/publications/permissions.js b/packages/rocketchat-authorization/server/publications/permissions.js index 5963b532a048..f6a5901f153e 100644 --- a/packages/rocketchat-authorization/server/publications/permissions.js +++ b/packages/rocketchat-authorization/server/publications/permissions.js @@ -51,5 +51,8 @@ Meteor.methods({ RocketChat.models.Permissions.on('changed', (type, permission) => { RocketChat.Notifications.notifyLoggedInThisInstance('permissions-changed', type, permission); + if (permission.level === permissionLevel.SETTING) { + RocketChat.Notifications.notifyLoggedInThisInstance('selected-settings-changed', type, permission); + } }); diff --git a/packages/rocketchat-authorization/server/startup.js b/packages/rocketchat-authorization/server/startup.js index f8e23016b3cd..85bce3bc4451 100644 --- a/packages/rocketchat-authorization/server/startup.js +++ b/packages/rocketchat-authorization/server/startup.js @@ -64,7 +64,7 @@ Meteor.startup(function() { {_id: 'view-other-user-channels', roles: ['admin']}, {_id: 'view-p-room', roles: ['admin', 'user', 'anonymous']}, {_id: 'view-privileged-setting', roles: ['admin']}, - {_id: 'view-selected-settings', roles: ['admin']}, + {_id: 'manage-selected-settings', roles: ['admin']}, {_id: 'view-room-administration', roles: ['admin']}, {_id: 'view-statistics', roles: ['admin']}, {_id: 'view-user-administration', roles: ['admin']}, diff --git a/packages/rocketchat-lib/server/methods/saveSetting.js b/packages/rocketchat-lib/server/methods/saveSetting.js index 963876864e31..a52250ecb95a 100644 --- a/packages/rocketchat-lib/server/methods/saveSetting.js +++ b/packages/rocketchat-lib/server/methods/saveSetting.js @@ -8,9 +8,13 @@ Meteor.methods({ }); } - if (!RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting')) { + if (!RocketChat.authz.hasPermission(Meteor.userId(), 'edit-privileged-setting') + && !( + RocketChat.authz.hasAllPermission(Meteor.userId(), ['manage-selected-settings', `change-setting-${ _id }`]) + )) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { - method: 'saveSetting' + method: 'saveSetting', + settingId: _id }); } diff --git a/packages/rocketchat-lib/server/publications/settings.js b/packages/rocketchat-lib/server/publications/settings.js index 80b41e338cb3..f4b1787f9b01 100644 --- a/packages/rocketchat-lib/server/publications/settings.js +++ b/packages/rocketchat-lib/server/publications/settings.js @@ -31,42 +31,16 @@ Meteor.methods({ return []; } this.unblock(); - if (!RocketChat.authz.hasPermission(Meteor.userId(), 'view-privileged-setting')) { - return []; - } - const records = RocketChat.models.Settings.find().fetch().filter(function(record) { - return record.hidden !== true; - }); - if (updatedAt instanceof Date) { - return { - update: records.filter(function(record) { - return record._updatedAt > updatedAt; - }), - remove: RocketChat.models.Settings.trashFindDeletedAfter(updatedAt, { - hidden: { - $ne: true - } - }, { - fields: { - _id: 1, - _deletedAt: 1 - } - }).fetch() - }; - } - return records; - }, - 'selected-settings/get'(updatedAt) { - if (!Meteor.userId()) { - return []; - } - this.unblock(); - if (!RocketChat.authz.hasPermission(Meteor.userId(), 'view-selected-settings')) { - return []; - } const records = RocketChat.models.Settings.find().fetch().filter(function(record) { - return record.hidden !== true && RocketChat.authz.hasPermission(Meteor.userId(), `change-setting-${ record._id }`); + if (RocketChat.authz.hasPermission(Meteor.userId(), 'view-privileged-setting')) { + return record.hidden !== true; + } else if (RocketChat.authz.hasPermission(Meteor.userId(), 'manage-selected-settings')) { + return record.hidden !== true && RocketChat.authz.hasPermission(Meteor.userId(), `change-setting-${ record._id }`); + } else { + return false; + } }); + if (updatedAt instanceof Date) { return { update: records.filter(function(record) { diff --git a/packages/rocketchat-ui-admin/client/admin.html b/packages/rocketchat-ui-admin/client/admin.html index a63acd6c93c6..e360e0724672 100644 --- a/packages/rocketchat-ui-admin/client/admin.html +++ b/packages/rocketchat-ui-admin/client/admin.html @@ -15,7 +15,7 @@

- {{#unless hasPermission 'view-privileged-setting'}} + {{#unless hasSettingPermission}}

{{_ "You_are_not_authorized_to_view_this_page"}}

{{else}} {{#if description}} diff --git a/packages/rocketchat-ui-admin/client/admin.js b/packages/rocketchat-ui-admin/client/admin.js index d0155c2b608d..f43b9f469651 100644 --- a/packages/rocketchat-ui-admin/client/admin.js +++ b/packages/rocketchat-ui-admin/client/admin.js @@ -52,18 +52,8 @@ Template.admin.onCreated(function() { RocketChat.settings.collectionPrivate = RocketChat.settings.cachedCollectionPrivate.collection; RocketChat.settings.cachedCollectionPrivate.init(); } - - // settings which the user is explicitly allowed to change - if (RocketChat.settings.cachedCollectionSelected == null) { - RocketChat.settings.cachedCollectionSelected = new RocketChat.CachedCollection({ - name: 'selected-settings', - eventType: 'onLogged' - }); - RocketChat.settings.collectionSelected = RocketChat.settings.cachedCollectionSelected.collection; - RocketChat.settings.cachedCollectionSelected.init(); - } this.selectedRooms = new ReactiveVar({}); - const observation = { + RocketChat.settings.collectionPrivate.find().observe({ added: (data) => { const selectedRooms = this.selectedRooms.get(); if (data.type === 'roomPick') { @@ -88,10 +78,7 @@ Template.admin.onCreated(function() { } TempSettings.remove(data._id); } - }; - - RocketChat.settings.collectionPrivate.find().observe(observation); - RocketChat.settings.collectionSelected.find().observe(observation); + }); }); Template.admin.onDestroyed(function() { @@ -99,6 +86,9 @@ Template.admin.onDestroyed(function() { }); Template.admin.helpers({ + hasSettingPermission() { + return RocketChat.authz.hasAtLeastOnePermission(['view-privileged-setting', 'manage-selected-settings']); + }, languages() { const languages = TAPi18n.getLanguages(); diff --git a/packages/rocketchat-ui-admin/client/adminFlex.html b/packages/rocketchat-ui-admin/client/adminFlex.html index 98c3207f9b20..aa66794fe688 100644 --- a/packages/rocketchat-ui-admin/client/adminFlex.html +++ b/packages/rocketchat-ui-admin/client/adminFlex.html @@ -31,7 +31,7 @@

{{_ "Administration"}}

{{/each}} - {{#if hasPermission 'view-privileged-setting'}} + {{#if hasSettingPermission}}

{{_ "Settings"}}

- {{#if hasSettingPermission}} -
-
-
-
- {{_ "Setting_permissions"}}
-
- -
-
-
- {{#if settingPermissionExpanded }} - {{> permissionsTable permissions=settingPermissions allRoles=roles collection='Setting'}} - {{else}} - {{_ "Not_authorized"}} - {{/if}} + {{/if}} + {{#if hasSettingPermission}} +
+
+
+
+ {{_ "Setting_permissions"}}
+
+
+
+ {{#if settingPermissionExpanded }} + {{> permissionsTable permissions=settingPermissions allRoles=roles collection='Setting'}} + {{else}} + {{_ "Not_authorized"}} + {{/if}} +
- {{/if}} - {{else}} +
+ {{/if}} + {{#if hasNoPermission}} {{_ "Not_authorized"}} {{/if}}
diff --git a/packages/rocketchat-authorization/client/views/permissions.js b/packages/rocketchat-authorization/client/views/permissions.js index 0b46031509e6..45cfbd206dc2 100644 --- a/packages/rocketchat-authorization/client/views/permissions.js +++ b/packages/rocketchat-authorization/client/views/permissions.js @@ -43,6 +43,10 @@ Template.permissions.helpers({ return RocketChat.authz.hasAllPermission('access-setting-permissions'); }, + hasNoPermission() { + return !RocketChat.authz.hasAtLeastOnePermission(['access-permissions', 'access-setting-permissions']); + }, + settingPermissionExpanded() { return Template.instance().settingPermissionsExpanded.get(); }, diff --git a/packages/rocketchat-ui-sidenav/client/accountBox.js b/packages/rocketchat-ui-sidenav/client/accountBox.js deleted file mode 100644 index 9e5c94d5e861..000000000000 --- a/packages/rocketchat-ui-sidenav/client/accountBox.js +++ /dev/null @@ -1,132 +0,0 @@ -/* globals popover isRtl */ - -Template.accountBox.helpers({ - myUserInfo() { - if (Meteor.user() == null && RocketChat.settings.get('Accounts_AllowAnonymousRead')) { - return { - name: t('Anonymous'), - fname: RocketChat.settings.get('UI_Use_Real_Name') && t('Anonymous'), - status: 'online', - visualStatus: t('online'), - bullet: 'general-success-background', - username: 'anonymous', - }; - } - - const user = Meteor.user() || {}; - const { name, username } = user; - const userStatus = Session.get(`user_${ username }_status`); - - return { - name: Session.get(`user_${ username }_name`) || username, - status: Session.get(`user_${ username }_status`), - visualStatus: t(userStatus.charAt(0).toUpperCase() + userStatus.slice(1)), - bullet: userStatus, - _id: Meteor.userId(), - username, - fname: RocketChat.settings.get('UI_Use_Real_Name') && name, - }; - }, - - isAnonymous() { - if (Meteor.userId() == null && RocketChat.settings.get('Accounts_AllowAnonymousRead')) { - return 'disabled'; - } - }, -}); - -Template.accountBox.events({ - 'click .sidebar__account.active'() { - let adminOption; - if (RocketChat.authz.hasAtLeastOnePermission(['view-statistics', 'view-room-administration', 'view-user-administration', 'view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']) || (RocketChat.AdminBox.getOptions().length > 0)) { - adminOption = { - icon: 'customize', - name: t('Administration'), - type: 'open', - id: 'administration', - }; - } - - const accountBox = document.querySelector('.sidebar__account'); - - const config = { - popoverClass: 'account', - columns: [ - { - groups: [ - { - title: t('User'), - items: [ - { - icon: 'circle', - name: t('Online'), - type: 'set-state', - id: 'online', - modifier: 'online', - }, - { - icon: 'circle', - name: t('Away'), - type: 'set-state', - id: 'away', - modifier: 'away', - }, - { - icon: 'circle', - name: t('Busy'), - type: 'set-state', - id: 'busy', - modifier: 'busy', - }, - { - icon: 'circle', - name: t('Invisible'), - type: 'set-state', - id: 'offline', - modifier: 'offline', - }, - ], - }, - { - items: AccountBox.getItems().map((item) => ({ - icon: item.icon, - name: t(item.name), - type: 'open', - id: item.name, - href: item.href, - sideNav: item.sideNav, - })).concat([ - adminOption, - { - icon: 'user', - name: t('My_Account'), - type: 'open', - id: 'account', - }, - { - icon: 'sign-out', - name: t('Logout'), - type: 'open', - id: 'logout', - }, - ]), - }, - - ], - }, - ], - position: { - top: accountBox.offsetHeight, - }, - customCSSProperties: { - width: `${ accountBox.offsetWidth - parseInt(getComputedStyle(accountBox)['padding-left'].replace('px', '')) * 2 }px`, - left: isRtl() ? 'auto' : getComputedStyle(accountBox)['padding-left'], - right: 'auto', - }, - }; - - popover.open(config); - }, -}); - -Template.accountBox.onRendered(() => AccountBox.init()); diff --git a/packages/rocketchat-ui-sidenav/client/sidebarHeader.js b/packages/rocketchat-ui-sidenav/client/sidebarHeader.js index 4140ec33204d..ecd2475c8408 100644 --- a/packages/rocketchat-ui-sidenav/client/sidebarHeader.js +++ b/packages/rocketchat-ui-sidenav/client/sidebarHeader.js @@ -139,10 +139,10 @@ const toolbarButtons = (user) => [{ { name: t('Options'), icon: 'menu', - condition: () => AccountBox.getItems().length || RocketChat.authz.hasAtLeastOnePermission(['manage-emoji', 'manage-integrations', 'manage-oauth-apps', 'manage-own-integrations', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration', 'manage-selected-settings']), + condition: () => AccountBox.getItems().length || RocketChat.authz.hasAtLeastOnePermission(['access-permissions', 'access-setting-permissions', 'edit-privileged-setting', 'manage-emoji', 'manage-integrations', 'manage-oauth-apps', 'manage-own-integrations', 'manage-selected-settings', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration']), action: (e) => { let adminOption; - if (RocketChat.authz.hasAtLeastOnePermission(['manage-emoji', 'manage-integrations', 'manage-oauth-apps', 'manage-own-integrations', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration', 'manage-selected-settings'])) { + if (RocketChat.authz.hasAtLeastOnePermission(['access-permissions', 'access-setting-permissions', 'edit-privileged-setting', 'manage-emoji', 'manage-integrations', 'manage-oauth-apps', 'manage-own-integrations', 'manage-selected-settings', 'manage-sounds', 'view-logs', 'view-privileged-setting', 'view-room-administration', 'view-statistics', 'view-user-administration'])) { adminOption = { icon: 'customize', name: t('Administration'), From 200902da54878eb36554a4601b127d83c19c1118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Tue, 25 Sep 2018 17:03:10 +0200 Subject: [PATCH 20/39] stylint --- .../rocketchat-authorization/client/stylesheets/permissions.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rocketchat-authorization/client/stylesheets/permissions.css b/packages/rocketchat-authorization/client/stylesheets/permissions.css index 30b4e0ea61e3..ce176b2276dd 100644 --- a/packages/rocketchat-authorization/client/stylesheets/permissions.css +++ b/packages/rocketchat-authorization/client/stylesheets/permissions.css @@ -11,7 +11,7 @@ & .section:not(.section-collapsed) { inline-size: fit-content; - } + } & .permission-grid { & th { From b138793ba4281165fdb456f22d3fdce85e256e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20J=C3=A4gle?= Date: Tue, 25 Sep 2018 20:35:07 +0200 Subject: [PATCH 21/39] Reduce footprint of PR - remove everything not related to actual feature --- .gitignore | 1 - .scripts/continueTesting.sh | 33 - .scripts/seperateTesting.sh | 23 - package.json | 1 - .../rocketchat-livechat/app/package-lock.json | 879 ------------------ 5 files changed, 937 deletions(-) delete mode 100755 .scripts/continueTesting.sh delete mode 100755 .scripts/seperateTesting.sh delete mode 100644 packages/rocketchat-livechat/app/package-lock.json diff --git a/.gitignore b/.gitignore index d8df1bd2807a..ae7a0245e404 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,3 @@ settings.json build.sh /public/livechat packages/rocketchat-i18n/i18n/livechat.* -tests/end-to-end/temporary_staged_test/* diff --git a/.scripts/continueTesting.sh b/.scripts/continueTesting.sh deleted file mode 100755 index 19a0e174e56a..000000000000 --- a/.scripts/continueTesting.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -tmpPath=tests/end-to-end/temporary_staged_test -stopfile=`find ${tmpPath} -type f | head -1` -echo 'Last stop at:' $stopfile -[ -z "$RETRY_TESTS" ] && RETRY_TESTS=1 -stopfile=`find ${tmpPath} -type f | head -1` -array=(`find tests/end-to-end/*/*.js -type f`) - -for j in ${!array[@]}; do - file=${array[$j]} - [[ ${stopfile##*/} == ${file##*/} ]] && [[ $stopfile != $file ]] && break -done - -rm -rf $tmpPath -mkdir -p $tmpPath -for file in ${array[@]:$j}; do - failed=1 - for i in `seq 1 $RETRY_TESTS`; do - echo '-------------- '$i' try ---------------' - set -x - cp $file $tmpPath - CHIMP_PATH=$tmpPath npm run chimp-path - failed=$? - set +x - if [ $failed -eq 0 ]; then - break - fi - done - if [ $failed -ne 0 ]; then - exit 1 - fi - rm $tmpPath/${file##*/} -done diff --git a/.scripts/seperateTesting.sh b/.scripts/seperateTesting.sh deleted file mode 100755 index 47e477bb5465..000000000000 --- a/.scripts/seperateTesting.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -tmpPath=tests/end-to-end/temporary_staged_test -rm -rf $tmpPath -mkdir -p $tmpPath -[ -z "$RETRY_TESTS" ] && RETRY_TESTS=1 -for file in tests/end-to-end/*/*.js; do - failed=1 - for i in `seq 1 $RETRY_TESTS`; do - echo '-------------- '$i' try ---------------' - set -x - cp $file $tmpPath - CHIMP_PATH=$tmpPath npm run chimp-path - failed=$? - set +x - if [ $failed -eq 0 ]; then - break - fi - done - if [ $failed -ne 0 ]; then - exit 1 - fi - rm $tmpPath/${file##*/} -done diff --git a/package.json b/package.json index 6eebd338155d..0f2f59825e41 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,6 @@ "jslint": "eslint .", "stylelint": "stylelint \"packages/**/*.css\"", "test": "node .scripts/start.js", - "chimp-path": "chimp tests/chimp-config.js --path=$CHIMP_PATH", "chimp-watch": "chimp --ddp=http://localhost:3000 --watch --mocha --path=tests/end-to-end", "chimp-test": "chimp tests/chimp-config.js", "postinstall": "cd packages/rocketchat-katex && npm i", diff --git a/packages/rocketchat-livechat/app/package-lock.json b/packages/rocketchat-livechat/app/package-lock.json deleted file mode 100644 index e80f4df03095..000000000000 --- a/packages/rocketchat-livechat/app/package-lock.json +++ /dev/null @@ -1,879 +0,0 @@ -{ - "name": "rocketchat-livechat", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=" - }, - "ajv": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz", - "integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=", - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.3.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=" - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.3.3" - } - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "autolinker": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-1.6.0.tgz", - "integrity": "sha1-utN2t62OQV8i8QL8Dzf2QOZPHL8=" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", - "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.1", - "regenerator-runtime": "0.11.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "bcrypt": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", - "integrity": "sha512-pRyDdo73C8Nim3jwFJ7DWe3TZCgwDfWZ6nHS5LSdU77kWbj1frruvdndP02AOavtD4y8v6Fp2dolbHgp4SDrfg==", - "requires": { - "nan": "2.6.2", - "node-pre-gyp": "0.6.36" - } - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.2.0" - } - }, - "brace-expansion": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", - "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "core-js": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", - "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.2.0" - }, - "dependencies": { - "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=", - "requires": { - "hoek": "4.2.0" - } - } - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=", - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", - "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", - "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.2" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", - "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "1.2.0", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "1.0.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", - "requires": { - "ajv": "5.3.0", - "har-schema": "2.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "hawk": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", - "integrity": "sha1-r02RTrBl+bXOTZ0RwcshJu7MMDg=", - "requires": { - "boom": "4.3.1", - "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.1.0" - } - }, - "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha1-ctnQdU9/4lyi0BrY+PmpRJqJUm0=" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.13.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jquery": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz", - "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" - }, - "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", - "requires": { - "mime-db": "1.30.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "1.1.8" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "moment": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", - "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "nan": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", - "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" - }, - "node-pre-gyp": { - "version": "0.6.36", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", - "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", - "requires": { - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.2", - "rc": "1.2.2", - "request": "2.83.0", - "rimraf": "2.6.2", - "semver": "5.4.1", - "tar": "2.2.1", - "tar-pack": "3.4.1" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "requires": { - "abbrev": "1.1.1", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha1-NJzfbu+J7EXBLX1es/wMhwNDptg=" - }, - "rc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz", - "integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=", - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "readable-stream": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", - "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "safe-buffer": "5.1.1", - "string_decoder": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "util-deprecate": "1.0.2" - } - }, - "regenerator-runtime": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", - "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" - }, - "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha1-ygtl2gLtYpNYh4COb1EDgQNOM1Y=", - "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.3.1", - "har-validator": "5.0.3", - "hawk": "6.0.2", - "http-signature": "1.2.0", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", - "oauth-sign": "0.8.2", - "performance-now": "2.1.0", - "qs": "6.5.1", - "safe-buffer": "5.1.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.3", - "tunnel-agent": "0.6.0", - "uuid": "3.1.0" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "sntp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", - "integrity": "sha1-LGzsFP7cIiJznK+bXD2F0cxaLMg=", - "requires": { - "hoek": "4.2.0" - } - }, - "sprintf-js": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", - "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" - }, - "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - } - }, - "string-width": { - "version": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "string_decoder": { - "version": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha1-D8Z9fBQYJd6UKC3VNr7GubzoYKs=", - "requires": { - "safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz" - } - }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "tar": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", - "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", - "integrity": "sha1-4dvAOpudO6B+iWrQJzF+tnmhCh8=", - "requires": { - "debug": "2.6.9", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.3.3", - "rimraf": "2.6.2", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "toastr": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", - "integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=", - "requires": { - "jquery": "3.2.1" - } - }, - "tough-cookie": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", - "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "5.1.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", - "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" - }, - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" - }, - "underscore.string": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.4.tgz", - "integrity": "sha1-LCo/n4PmR2L9xF5s6sZRQoZCE9s=", - "requires": { - "sprintf-js": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "1.3.0" - } - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "requires": { - "string-width": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - } - } -} From b2e901ab3ca0b56dc6a5d356e29f3a9c83ffc097 Mon Sep 17 00:00:00 2001 From: mrsimpson Date: Wed, 7 Aug 2019 21:49:35 +0200 Subject: [PATCH 22/39] [WIP] - merge latest develop --- .circleci/config.yml | 398 +- .circleci/setartname.sh | 2 +- .circleci/sign.key.gpg | Bin 5117 -> 21432 bytes .circleci/snap.sh | 8 +- .circleci/update-releases.sh | 3 + .docker-mongo/Dockerfile | 4 +- .docker/Dockerfile.local | 20 - .docker/Dockerfile.rhel | 2 +- .eslintignore | 23 +- .eslintrc | 71 +- .github/ISSUE_TEMPLATE/feature_request.md | 17 - .github/bot-config.yml | 1 + .github/changelog.js | 116 - .github/history-manual.json | 5 + .github/history.json | 14781 ++++++++++ .github/templates/commit.hbs | 40 - .github/templates/footer.hbs | 11 - .github/templates/header.hbs | 26 - .github/templates/template.hbs | 22 - .gitignore | 3 + .meteor/packages | 195 +- .meteor/platforms | 1 - .meteor/release | 2 +- .meteor/versions | 258 +- .postcssrc | 18 +- .sandstorm/.gitignore | 1 - .sandstorm/CHANGELOG.md | 1 - .sandstorm/README.md | 14 - .sandstorm/Vagrantfile | 104 - .sandstorm/build.sh | 23 - .sandstorm/description.md | 1 - .sandstorm/global-setup.sh | 34 - .sandstorm/launcher.sh | 8 - .sandstorm/pgp-keyring | Bin 8435 -> 0 bytes .sandstorm/pgp-signature | Bin 688 -> 0 bytes .sandstorm/rocket.chat-128.svg | 52 - .sandstorm/rocket.chat-150.svg | 37 - .sandstorm/rocket.chat-24.svg | 32 - .sandstorm/sandstorm-pkgdef.capnp | 115 - .sandstorm/screenshot1.png | Bin 40830 -> 0 bytes .sandstorm/screenshot2.png | Bin 60942 -> 0 bytes .sandstorm/screenshot3.png | Bin 71990 -> 0 bytes .sandstorm/screenshot4.png | Bin 219976 -> 0 bytes .sandstorm/setup.sh | 66 - .sandstorm/stack | 1 - .scripts/fix-i18n.js | 29 + .scripts/logs.js | 3 +- .scripts/md.js | 7 +- .scripts/npm-postinstall.js | 13 + .scripts/set-version.js | 8 +- .scripts/start.js | 70 +- .scripts/translationDiff.js | 38 + .scripts/version.js | 5 +- .snapcraft/resources/Caddyfile | 4 +- .snapcraft/resources/initcaddy | 49 +- .snapcraft/resources/prepareRocketChat | 3 + .snapcraft/resources/preparemongo | 4 +- .snapcraft/resources/startRocketChat | 16 +- .snapcraft/snap/hooks/configure | 122 + .snapcraft/snap/hooks/install | 16 + .snapcraft/snap/hooks/post-refresh | 32 + .snapcraft/{ => snap}/snapcraft.yaml | 14 +- .stylelintignore | 4 +- .travis.yml | 1 - .travis/sandstorm.sh | 37 - .travis/snap.sh | 2 +- .vscode/launch.json | 26 +- HISTORY.md | 2626 +- LIMITATION_OF_RESPONSIBILITY.md | 23 + README.md | 95 +- app.json | 2 +- .../rocketchat-2fa => app/2fa}/README.md | 0 .../2fa}/client/TOTPPassword.js | 5 + app/2fa/client/accountSecurity.html | 47 + .../2fa}/client/accountSecurity.js | 9 +- app/2fa/client/index.js | 3 + app/2fa/server/index.js | 7 + .../2fa}/server/lib/totp.js | 11 +- app/2fa/server/loginHandler.js | 39 + .../server/methods/checkCodesRemaining.js | 2 + app/2fa/server/methods/disable.js | 27 + app/2fa/server/methods/enable.js | 23 + app/2fa/server/methods/regenerateCodes.js | 32 + app/2fa/server/methods/validateTempToken.js | 30 + app/2fa/server/startup/settings.js | 19 + .../accounts}/README.md | 0 app/accounts/index.js | 1 + .../accounts}/server/config.js | 0 .../accounts}/server/index.js | 0 .../action-links}/README.md | 0 app/action-links/both/lib/actionLinks.js | 36 + app/action-links/client/index.js | 7 + app/action-links/client/init.js | 33 + app/action-links/client/lib/actionLinks.js | 27 + .../client/stylesheets/actionLinks.css | 0 app/action-links/index.js | 8 + app/action-links/server/actionLinkHandler.js | 18 + app/action-links/server/index.js | 6 + .../analytics}/README.md | 0 app/analytics/client/index.js | 2 + .../analytics}/client/loadScript.js | 20 +- app/analytics/client/trackEvents.js | 151 + app/analytics/server/index.js | 1 + app/analytics/server/settings.js | 87 + app/api/index.js | 1 + app/api/server/api.js | 573 + app/api/server/default/info.js | 19 + .../api}/server/helpers/README.md | 0 .../helpers/composeRoomWithLastMessage.js | 10 + app/api/server/helpers/deprecationWarning.js | 14 + app/api/server/helpers/getLoggedInUser.js | 17 + app/api/server/helpers/getPaginationItems.js | 32 + app/api/server/helpers/getUserFromParams.js | 27 + app/api/server/helpers/getUserInfo.js | 37 + app/api/server/helpers/insertUserObject.js | 17 + app/api/server/helpers/isUserFromParams.js | 10 + app/api/server/helpers/parseJsonQuery.js | 86 + app/api/server/helpers/requestParams.js | 5 + app/api/server/index.js | 34 + app/api/server/settings.js | 14 + app/api/server/v1/assets.js | 57 + app/api/server/v1/channels.js | 1047 + app/api/server/v1/chat.js | 570 + app/api/server/v1/commands.js | 171 + app/api/server/v1/e2e.js | 62 + app/api/server/v1/emoji-custom.js | 159 + app/api/server/v1/groups.js | 815 + app/api/server/v1/im.js | 369 + app/api/server/v1/import.js | 51 + app/api/server/v1/integrations.js | 156 + app/api/server/v1/misc.js | 204 + app/api/server/v1/permissions.js | 120 + app/api/server/v1/push.js | 65 + app/api/server/v1/roles.js | 88 + app/api/server/v1/rooms.js | 272 + app/api/server/v1/settings.js | 142 + app/api/server/v1/stats.js | 48 + app/api/server/v1/subscriptions.js | 85 + app/api/server/v1/users.js | 676 + app/api/server/v1/video-conference.js | 22 + .../rocketchat-apps => app/apps}/.gitignore | 0 .../rocketchat-apps => app/apps}/README.md | 0 app/apps/assets/stylesheets/apps.css | 381 + app/apps/client/admin/appInstall.html | 66 + .../apps}/client/admin/appInstall.js | 22 +- app/apps/client/admin/appLogs.html | 55 + app/apps/client/admin/appLogs.js | 115 + app/apps/client/admin/appManage.css | 181 + app/apps/client/admin/appManage.html | 522 + app/apps/client/admin/appManage.js | 501 + .../apps}/client/admin/appWhatIsIt.html | 0 app/apps/client/admin/appWhatIsIt.js | 55 + app/apps/client/admin/apps.html | 128 + app/apps/client/admin/apps.js | 284 + app/apps/client/admin/helpers.js | 379 + app/apps/client/admin/marketplace.html | 145 + app/apps/client/admin/marketplace.js | 333 + .../admin/modalTemplates/iframeModal.html | 7 + .../admin/modalTemplates/iframeModal.js | 49 + app/apps/client/communication/index.js | 1 + app/apps/client/communication/websockets.js | 58 + app/apps/client/i18n.js | 38 + app/apps/client/index.js | 14 + app/apps/client/orchestrator.js | 188 + app/apps/client/routes.js | 55 + app/apps/lib/misc/Utilities.js | 72 + app/apps/lib/misc/transformMappedData.js | 85 + .../apps}/server/bridges/activation.js | 0 app/apps/server/bridges/api.js | 114 + .../apps}/server/bridges/bridges.js | 14 +- app/apps/server/bridges/commands.js | 173 + .../apps}/server/bridges/details.js | 0 app/apps/server/bridges/environmental.js | 32 + app/apps/server/bridges/http.js | 21 + app/apps/server/bridges/index.js | 27 + app/apps/server/bridges/internal.js | 27 + app/apps/server/bridges/listeners.js | 39 + app/apps/server/bridges/messages.js | 84 + app/apps/server/bridges/persistence.js | 110 + app/apps/server/bridges/rooms.js | 124 + app/apps/server/bridges/settings.js | 58 + app/apps/server/bridges/users.js | 23 + .../apps}/server/communication/index.js | 0 app/apps/server/communication/methods.js | 94 + app/apps/server/communication/rest.js | 667 + app/apps/server/communication/websockets.js | 183 + .../apps}/server/converters/index.js | 0 app/apps/server/converters/messages.js | 238 + app/apps/server/converters/rooms.js | 95 + app/apps/server/converters/settings.js | 53 + app/apps/server/converters/users.js | 80 + app/apps/server/cron.js | 116 + app/apps/server/index.js | 3 + app/apps/server/orchestrator.js | 174 + app/apps/server/storage/index.js | 4 + .../apps}/server/storage/logs-storage.js | 0 .../apps}/server/storage/storage.js | 11 +- app/apps/server/tests/messages.tests.js | 153 + .../server/tests/mocks/data/messages.data.js | 115 + .../tests/mocks/models/BaseModel.mock.js | 5 + .../tests/mocks/models/Messages.mock.js | 36 + .../server/tests/mocks/models/Rooms.mock.js | 127 + .../server/tests/mocks/models/Users.mock.js | 43 + app/apps/server/tests/mocks/models/index.js | 7 + .../server/tests/mocks/orchestrator.mock.js | 105 + app/assets/index.js | 1 + app/assets/server/assets.js | 533 + app/assets/server/index.js | 1 + .../authorization}/README.md | 0 app/authorization/client/hasPermission.js | 65 + app/authorization/client/hasRole.js | 6 + app/authorization/client/index.js | 18 + .../client/lib/ChatPermissions.js | 3 + .../client/requiresPermission.html | 0 app/authorization/client/route.js | 36 + app/authorization/client/startup.js | 18 + .../client/stylesheets/permissions.css | 122 + app/authorization/client/usersNameChanged.js | 18 + .../client/views/permissions.html | 76 + app/authorization/client/views/permissions.js | 166 + .../client/views/permissionsRole.html | 108 + .../client/views/permissionsRole.js | 279 + app/authorization/index.js | 8 + .../server/functions/addUserRoles.js | 32 + .../server/functions/canAccessRoom.js | 40 + .../server/functions/canSendMessage.js | 38 + .../server/functions/getRoles.js | 3 + .../server/functions/getUsersInRole.js | 3 + .../server/functions/hasPermission.js | 48 + app/authorization/server/functions/hasRole.js | 6 + .../server/functions/removeUserFromRoles.js | 33 + app/authorization/server/index.js | 41 + .../server/methods/addPermissionToRole.js | 31 + .../server/methods/addUserToRole.js | 66 + .../server/methods/deleteRole.js | 40 + .../methods/removeRoleFromPermission.js | 37 + .../server/methods/removeUserFromRole.js | 71 + app/authorization/server/methods/saveRole.js | 37 + .../server/publications/permissions.js | 46 + .../publications/permissions/emitter.js | 24 + .../server/publications/permissions/index.js | 22 + .../server/publications/roles.js | 11 + .../server/publications/usersInRole.js | 27 + app/authorization/server/startup.js | 186 + app/autolinker/client/client.js | 75 + app/autolinker/client/index.js | 1 + app/autolinker/server/index.js | 1 + app/autolinker/server/settings.js | 20 + .../autotranslate}/README.md | 0 app/autotranslate/client/index.js | 6 + app/autotranslate/client/lib/actionButton.js | 68 + app/autotranslate/client/lib/autotranslate.js | 128 + app/autotranslate/client/lib/tabBar.js | 22 + .../client/stylesheets/autotranslate.css | 0 .../client/views/autoTranslateFlexTab.html | 0 .../client/views/autoTranslateFlexTab.js | 18 +- app/autotranslate/server/autotranslate.js | 263 + app/autotranslate/server/index.js | 6 + .../server/methods/getSupportedLanguages.js | 23 + .../server/methods/saveSettings.js | 44 + .../server/methods/translateMessage.js | 13 + app/autotranslate/server/permissions.js | 11 + app/autotranslate/server/settings.js | 8 + app/bigbluebutton/index.js | 1 + .../server/bigbluebutton-api.js | 0 app/blockstack/client/index.js | 52 + app/blockstack/client/routes.js | 44 + .../main.js => app/blockstack/server/index.js | 0 app/blockstack/server/logger.js | 3 + app/blockstack/server/loginHandler.js | 54 + app/blockstack/server/routes.js | 28 + app/blockstack/server/settings.js | 70 + .../blockstack}/server/tokenHandler.js | 2 +- .../blockstack}/server/userHandler.js | 5 +- .../bot-helpers}/README.md | 0 app/bot-helpers/index.js | 1 + app/bot-helpers/server/index.js | 166 + app/bot-helpers/server/settings.js | 14 + app/callbacks/client/index.js | 5 + app/callbacks/index.js | 8 + app/callbacks/lib/callbacks.js | 149 + app/callbacks/server/index.js | 5 + app/cas/client/cas_client.js | 77 + app/cas/client/index.js | 1 + app/cas/server/cas_rocketchat.js | 69 + app/cas/server/cas_server.js | 268 + app/cas/server/index.js | 2 + .../client/index.js | 3 + .../client/lib/startup.js | 20 + .../client/resetSelection.js | 2 + .../views/mailMessagesInstructions.html | 14 +- .../client/views/mailMessagesInstructions.js | 46 +- .../server/index.js | 2 + .../server/lib/startup.js | 13 + .../server/methods/mailMessages.js | 92 + app/channel-settings/client/index.js | 7 + .../client/lib/ChannelSettings.js | 7 +- .../client/startup/messageTypes.js | 55 + app/channel-settings/client/startup/tabBar.js | 15 + .../client/startup/trackSettingsChange.js | 48 + .../client/stylesheets/channel-settings.css | 2 +- .../client/views/channelSettings.html | 37 +- .../client/views/channelSettings.js | 829 + app/channel-settings/index.js | 8 + .../server/functions/saveReactWhenReadOnly.js | 12 + .../server/functions/saveRoomAnnouncement.js | 25 + .../server/functions/saveRoomCustomFields.js | 23 + .../server/functions/saveRoomDescription.js | 16 + .../server/functions/saveRoomName.js | 33 + .../server/functions/saveRoomReadOnly.js | 14 + .../functions/saveRoomSystemMessages.js | 13 + .../server/functions/saveRoomTokens.js | 14 + .../server/functions/saveRoomTopic.js | 18 + .../server/functions/saveRoomType.js | 47 + .../server/functions/saveStreamingOptions.js | 23 + app/channel-settings/server/index.js | 6 + .../server/methods/saveRoomSettings.js | 227 + app/channel-settings/server/startup.js | 9 + app/chatpal-search/client/index.js | 7 + app/chatpal-search/client/route.js | 14 + .../chatpal-search/client/style.css | 0 .../chatpal-search/client/template/admin.html | 0 app/chatpal-search/client/template/admin.js | 66 + .../client/template/result.html | 153 + app/chatpal-search/client/template/result.js | 141 + .../client/template/suggestion.html | 0 .../client/template/suggestion.js | 2 + app/chatpal-search/server/asset/config.js | 4 + app/chatpal-search/server/index.js | 3 + app/chatpal-search/server/provider/index.js | 431 + .../server/provider/provider.js | 340 + app/chatpal-search/server/utils/logger.js | 4 + app/chatpal-search/server/utils/utils.js | 27 + app/cloud/client/admin/callback.html | 16 + app/cloud/client/admin/callback.js | 46 + app/cloud/client/admin/cloud.html | 138 + app/cloud/client/admin/cloud.js | 222 + app/cloud/client/index.js | 31 + .../functions/checkUserHasCloudLogin.js | 22 + .../server/functions/connectWorkspace.js | 64 + .../server/functions/disconnectWorkspace.js | 13 + .../functions/finishOAuthAuthorization.js | 59 + .../functions/getOAuthAuthorizationUrl.js | 20 + app/cloud/server/functions/getRedirectUri.js | 5 + .../functions/getUserCloudAccessToken.js | 100 + .../functions/getWorkspaceAccessToken.js | 74 + app/cloud/server/functions/getWorkspaceKey.js | 18 + .../server/functions/getWorkspaceLicense.js | 46 + .../functions/retrieveRegistrationStatus.js | 22 + .../functions/startRegisterWorkspace.js | 87 + app/cloud/server/functions/syncWorkspace.js | 67 + .../server/functions/unregisterWorkspace.js | 20 + app/cloud/server/functions/userLoggedOut.js | 19 + app/cloud/server/functions/userLogout.js | 52 + app/cloud/server/index.js | 33 + app/cloud/server/methods.js | 138 + app/cloud/server/oauthScopes.js | 16 + app/colors/client/client.js | 23 + app/colors/client/index.js | 1 + .../colors}/client/style.css | 0 app/colors/server/index.js | 1 + app/colors/server/settings.js | 9 + app/cors/client/index.js | 1 + app/cors/lib/common.js | 9 + app/cors/server/cors.js | 111 + app/cors/server/index.js | 2 + app/crowd/client/index.js | 1 + app/crowd/client/loginHelper.js | 26 + app/crowd/server/crowd.js | 390 + app/crowd/server/index.js | 6 + app/crowd/server/settings.js | 22 + .../custom-oauth}/.gitignore | 0 .../custom-oauth}/README.md | 0 .../client/custom_oauth_client.js | 15 +- app/custom-oauth/index.js | 8 + .../server/custom_oauth_server.js | 465 + app/custom-oauth/server/oauth_helpers.js | 42 + .../assets/stylesheets/customSoundsAdmin.css | 0 .../client/admin/adminSoundEdit.html | 0 .../client/admin/adminSoundInfo.html | 0 .../client/admin/adminSounds.html | 75 + app/custom-sounds/client/admin/adminSounds.js | 165 + app/custom-sounds/client/admin/route.js | 13 + .../client/admin/soundEdit.html | 0 .../custom-sounds}/client/admin/soundEdit.js | 8 +- .../client/admin/soundInfo.html | 0 .../custom-sounds}/client/admin/soundInfo.js | 7 + app/custom-sounds/client/admin/startup.js | 11 + app/custom-sounds/client/index.js | 14 + app/custom-sounds/client/lib/CustomSounds.js | 71 + .../client/notifications/deleteCustomSound.js | 11 + .../client/notifications/updateCustomSound.js | 11 + app/custom-sounds/server/index.js | 8 + .../server/methods/deleteCustomSound.js | 28 + .../server/methods/insertOrUpdateSound.js | 68 + .../server/methods/listCustomSounds.js | 9 + .../server/methods/uploadCustomSound.js | 25 + .../server/publications/customSounds.js | 30 + .../server/startup/custom-sounds.js | 81 + .../server/startup/permissions.js | 9 + app/custom-sounds/server/startup/settings.js | 24 + .../client/createDiscussionMessageAction.js | 52 + .../client/discussionFromMessageBox.js | 35 + app/discussion/client/index.js | 16 + .../client/lib/discussionsOfRoom.js | 3 + .../lib/messageTypes/discussionMessage.js | 16 + .../client/public/stylesheets/discussion.css | 22 + app/discussion/client/tabBar.js | 14 + .../client/views/DiscussionList.html | 10 + app/discussion/client/views/DiscussionList.js | 29 + .../client/views/DiscussionTabbar.html | 24 + .../client/views/DiscussionTabbar.js | 55 + .../creationDialog/CreateDiscussion.html | 107 + .../views/creationDialog/CreateDiscussion.js | 308 + app/discussion/lib/discussionRoomType.js | 22 + app/discussion/server/authorization.js | 10 + app/discussion/server/config.js | 39 + .../server/hooks/joinDiscussionOnMessage.js | 22 + .../hooks/propagateDiscussionMetadata.js | 37 + app/discussion/server/index.js | 13 + .../server/methods/createDiscussion.js | 140 + app/discussion/server/permissions.js | 17 + .../discussionParentAutocomplete.js | 44 + .../server/publications/discussionsOfRoom.js | 32 + app/dolphin/client/index.js | 1 + .../dolphin/client}/login-button.css | 0 app/dolphin/lib/common.js | 70 + app/dolphin/server/index.js | 2 + app/dolphin/server/startup.js | 10 + .../drupal}/README.md | 0 app/drupal/client/index.js | 1 + .../drupal/client}/login-button.css | 0 app/drupal/lib/common.js | 44 + app/drupal/server/index.js | 2 + app/drupal/server/startup.js | 16 + app/e2e/client/accountEncryption.html | 51 + app/e2e/client/accountEncryption.js | 86 + app/e2e/client/events.js | 7 + app/e2e/client/helper.js | 117 + app/e2e/client/index.js | 1 + app/e2e/client/rocketchat.e2e.js | 529 + .../e2e}/client/rocketchat.e2e.room.js | 19 +- .../e2e}/client/stylesheets/e2e.less | 0 app/e2e/client/tabbar.js | 30 + app/e2e/server/index.js | 15 + app/e2e/server/methods/fetchMyKeys.js | 13 + .../methods/getUsersOfRoomWithoutKey.js | 27 + .../server/methods/requestSubscriptionKeys.js | 35 + app/e2e/server/methods/resetUserE2EKey.js | 27 + .../e2e}/server/methods/setRoomKeyID.js | 5 +- .../methods/setUserPublicAndPivateKeys.js | 15 + app/e2e/server/methods/updateGroupKey.js | 15 + app/e2e/server/settings.js | 11 + .../assets/stylesheets/emojiCustomAdmin.css | 0 app/emoji-custom/client/admin/adminEmoji.html | 76 + app/emoji-custom/client/admin/adminEmoji.js | 137 + .../client}/admin/adminEmojiEdit.html | 0 .../client}/admin/adminEmojiInfo.html | 0 .../emoji-custom/client}/admin/emojiEdit.html | 0 .../emoji-custom/client}/admin/emojiEdit.js | 6 +- .../emoji-custom/client}/admin/emojiInfo.html | 0 .../emoji-custom/client}/admin/emojiInfo.js | 26 +- .../client}/admin/emojiPreview.html | 0 app/emoji-custom/client/admin/route.js | 13 + app/emoji-custom/client/admin/startup.js | 11 + app/emoji-custom/client/index.js | 14 + app/emoji-custom/client/lib/emojiCustom.js | 184 + app/emoji-custom/client/lib/function-isSet.js | 20 + .../client/notifications/deleteEmojiCustom.js | 8 + .../client/notifications/updateEmojiCustom.js | 8 + app/emoji-custom/server/index.js | 7 + .../server/methods/deleteEmojiCustom.js | 28 + .../server/methods/insertOrUpdateEmoji.js | 114 + .../server/methods/listEmojiCustom.js | 9 + .../server/methods/uploadEmojiCustom.js | 29 + .../server/publications/fullEmojiData.js | 31 + .../server/startup/emoji-custom.js | 114 + app/emoji-custom/server/startup/settings.js | 24 + .../emoji-emojione}/.gitignore | 0 app/emoji-emojione/README.md | 18 + .../client/activity-sprites.css | 2177 ++ .../client/emojione-sprites.css | 35 + app/emoji-emojione/client/flags-sprites.css | 1615 ++ app/emoji-emojione/client/food-sprites.css | 637 + app/emoji-emojione/client/index.js | 1 + .../client/modifier-sprites.css | 42 + app/emoji-emojione/client/nature-sprites.css | 1069 + app/emoji-emojione/client/objects-sprites.css | 1259 + app/emoji-emojione/client/people-sprites.css | 8729 ++++++ .../client/regional-sprites.css | 163 + app/emoji-emojione/client/symbols-sprites.css | 1747 ++ app/emoji-emojione/client/travel-sprites.css | 739 + app/emoji-emojione/lib/emojiPicker.js | 1957 ++ app/emoji-emojione/lib/emojione.tpl | 30 + app/emoji-emojione/lib/emojioneRender.js | 9 + app/emoji-emojione/lib/generateEmojiIndex.mjs | 185 + app/emoji-emojione/lib/rocketchat.js | 294 + app/emoji-emojione/server/callbacks.js | 8 + app/emoji-emojione/server/index.js | 2 + app/emoji/client/emojiParser.js | 79 + app/emoji/client/emojiPicker.html | 49 + app/emoji/client/emojiPicker.js | 268 + app/emoji/client/function-isSet.js | 20 + app/emoji/client/index.js | 12 + app/emoji/client/lib/EmojiPicker.js | 192 + app/emoji/client/lib/emojiRenderer.js | 27 + app/emoji/index.js | 8 + app/emoji/lib/rocketchat.js | 33 + app/emoji/server/index.js | 1 + app/error-handler/index.js | 1 + app/error-handler/server/index.js | 6 + .../server/lib/RocketChat.ErrorHandler.js | 75 + app/error-handler/server/startup/settings.js | 5 + .../favico}/client/favico.js | 20 +- app/favico/client/index.js | 3 + app/favico/index.js | 1 + app/federation/README.md | 1 + app/federation/client/admin/dashboard.css | 141 + app/federation/client/admin/dashboard.html | 33 + app/federation/client/admin/dashboard.js | 92 + app/federation/client/index.js | 2 + app/federation/client/messageTypes.js | 23 + app/federation/server/PeerClient.js | 638 + app/federation/server/PeerDNS.js | 185 + app/federation/server/PeerHTTP/PeerHTTP.js | 100 + app/federation/server/PeerHTTP/index.js | 1 + app/federation/server/PeerHTTP/utils.js | 16 + app/federation/server/PeerPinger.js | 37 + .../server/PeerServer/PeerServer.js | 404 + app/federation/server/PeerServer/index.js | 6 + .../server/PeerServer/routes/events.js | 115 + .../server/PeerServer/routes/uploads.js | 28 + .../server/PeerServer/routes/users.js | 48 + app/federation/server/config.js | 74 + .../federatedResources/FederatedMessage.js | 263 + .../federatedResources/FederatedResource.js | 17 + .../federatedResources/FederatedRoom.js | 275 + .../federatedResources/FederatedUser.js | 125 + .../server/federatedResources/index.js | 4 + app/federation/server/federation-settings.js | 76 + app/federation/server/index.js | 149 + app/federation/server/logger.js | 13 + app/federation/server/methods/addUser.js | 50 + app/federation/server/methods/dashboard.js | 76 + app/federation/server/methods/ping.js | 54 + app/federation/server/methods/searchUsers.js | 21 + app/federation/server/settingsUpdater.js | 17 + app/file-upload/client/index.js | 5 + .../client/lib/fileUploadHandler.js | 33 + app/file-upload/index.js | 8 + app/file-upload/lib/FileUploadBase.js | 81 + app/file-upload/server/config/AmazonS3.js | 108 + .../file-upload}/server/config/FileSystem.js | 17 +- .../server/config/GoogleStorage.js | 22 +- .../file-upload}/server/config/GridFS.js | 8 +- app/file-upload/server/config/Webdav.js | 62 + .../server/config/_configUploadStorage.js | 22 + app/file-upload/server/index.js | 12 + app/file-upload/server/lib/FileUpload.js | 489 + app/file-upload/server/lib/proxy.js | 97 + app/file-upload/server/lib/requests.js | 26 + .../server/methods/getS3FileUrl.js | 22 + .../server/methods/sendFileMessage.js | 89 + app/file-upload/server/startup/settings.js | 247 + app/file-upload/ufs/AmazonS3/server.js | 159 + app/file-upload/ufs/GoogleStorage/server.js | 124 + app/file-upload/ufs/Webdav/server.js | 136 + app/file/index.js | 1 + .../file/server}/file.server.js | 17 +- app/file/server/index.js | 3 + .../github-enterprise-login-button.css | 0 app/github-enterprise/client/index.js | 1 + app/github-enterprise/lib/common.js | 39 + app/github-enterprise/server/index.js | 2 + app/github-enterprise/server/startup.js | 16 + .../gitlab/client}/gitlab-login-button.css | 0 app/gitlab/client/index.js | 1 + app/gitlab/lib/common.js | 59 + app/gitlab/server/index.js | 2 + app/gitlab/server/startup.js | 18 + .../google-vision}/README.md | 0 app/google-vision/client/googlevision.js | 90 + app/google-vision/client/index.js | 1 + app/google-vision/server/googlevision.js | 163 + app/google-vision/server/index.js | 2 + app/google-vision/server/settings.js | 93 + .../grant-facebook}/README.md | 0 app/grant-facebook/index.js | 1 + app/grant-facebook/server/index.js | 57 + .../grant-github}/README.md | 0 app/grant-github/index.js | 1 + app/grant-github/server/index.js | 47 + .../grant-google}/README.md | 0 app/grant-google/index.js | 1 + app/grant-google/server/index.js | 39 + .../rocketchat-grant => app/grant}/README.md | 0 app/grant/index.js | 1 + .../grant}/server/authenticate.js | 23 +- app/grant/server/error.js | 2 + .../grant}/server/grant.js | 5 +- app/grant/server/index.js | 58 + app/grant/server/providers.js | 40 + .../grant}/server/redirect.js | 0 .../grant}/server/routes.js | 0 app/grant/server/settings.js | 43 + .../grant}/server/storage.js | 0 .../graphql}/README.md | 0 app/graphql/index.js | 1 + app/graphql/server/api.js | 79 + .../graphql}/server/helpers/authenticated.js | 2 +- .../graphql}/server/helpers/dateToFloat.js | 0 app/graphql/server/index.js | 2 + .../server/mocks/accounts/graphql-api.js | 6 +- .../resolvers/accounts/OauthProvider-type.js | 0 .../server/resolvers/accounts/index.js | 22 + .../resolvers/accounts/oauthProviders.js | 5 +- .../server/resolvers/channels/Channel-type.js | 51 + .../resolvers/channels/ChannelFilter-input.js | 0 .../channels/ChannelNameAndDirect-input.js | 0 .../resolvers/channels/ChannelSort-enum.js | 0 .../server/resolvers/channels/Privacy-enum.js | 0 .../resolvers/channels/channelByName.js | 7 +- .../server/resolvers/channels/channels.js | 55 + .../resolvers/channels/channelsByUser.js | 31 + .../resolvers/channels/createChannel.js | 39 + .../resolvers/channels/deleteChannel.js | 41 + .../resolvers/channels/directChannel.js | 7 +- .../server/resolvers/channels/hideChannel.js | 6 +- .../server/resolvers/channels/index.js | 52 + .../server/resolvers/channels/leaveChannel.js | 4 +- .../server/resolvers/channels/settings.js | 0 .../server/resolvers/messages/Message-type.js | 10 +- .../messages/MessageIdentifier-input.js | 0 .../messages/MessagesWithCursor-type.js | 0 .../resolvers/messages/Reaction-type.js | 0 .../messages/addReactionToMessage.js | 4 +- .../resolvers/messages/chatMessageAdded.js | 11 +- .../resolvers/messages/deleteMessage.js | 32 + .../server/resolvers/messages/editMessage.js | 6 +- .../server/resolvers/messages/index.js | 0 .../server/resolvers/messages/messages.js | 90 + .../server/resolvers/messages/sendMessage.js | 27 + .../server/resolvers/users/User-type.js | 29 + .../server/resolvers/users/UserStatus-enum.js | 0 .../graphql}/server/resolvers/users/index.js | 0 .../server/resolvers/users/setStatus.js | 22 + .../graphql}/server/schema.js | 0 .../accounts/LoginResult-type.graphqls | 0 .../accounts/OauthProvider-type.graphqls | 0 .../schemas/accounts/oauthProviders.graphqls | 0 .../schemas/channels/Channel-type.graphqls | 0 .../channels/ChannelFilter-input.graphqls | 0 .../ChannelNameAndDirect-input.graphqls | 0 .../channels/ChannelSort-enum.graphqls | 0 .../schemas/channels/Privacy-enum.graphqls | 0 .../schemas/channels/channelByName.graphqls | 0 .../server/schemas/channels/channels.graphqls | 0 .../schemas/channels/channelsByUser.graphqls | 0 .../schemas/channels/createChannel.graphqls | 0 .../schemas/channels/deleteChannel.graphqls | 3 + .../schemas/channels/directChannel.graphqls | 0 .../schemas/channels/hideChannel.graphqls | 0 .../schemas/channels/leaveChannel.graphqls | 0 .../schemas/messages/Message-type.graphqls | 0 .../messages/MessageIdentifier-input.graphqls | 0 .../messages/MessagesWithCursor-type.graphqls | 0 .../schemas/messages/Reaction-type.graphqls | 0 .../messages/addReactionToMessage.graphqls | 0 .../messages/chatMessageAdded.graphqls | 0 .../schemas/messages/deleteMessage.graphqls | 0 .../schemas/messages/editMessage.graphqls | 0 .../server/schemas/messages/messages.graphqls | 0 .../schemas/messages/sendMessage.graphqls | 0 .../server/schemas/users/User-type.graphqls | 0 .../schemas/users/UserStatus-enum.graphqls | 0 .../server/schemas/users/setStatus.graphqls | 0 app/graphql/server/settings.js | 9 + .../graphql}/server/subscriptions.js | 0 app/highlight-words/client/client.js | 39 + app/highlight-words/client/helper.js | 27 + app/highlight-words/client/index.js | 1 + app/highlight-words/index.js | 1 + app/highlight-words/tests/helper.tests.js | 53 + app/iframe-login/client/iframe_client.js | 43 + app/iframe-login/client/index.js | 1 + .../iframe-login/server}/iframe_rocketchat.js | 6 +- app/iframe-login/server/iframe_server.js | 31 + app/iframe-login/server/index.js | 2 + app/importer-csv/client/adder.js | 4 + app/importer-csv/client/index.js | 1 + app/importer-csv/lib/info.js | 10 + app/importer-csv/server/adder.js | 5 + app/importer-csv/server/importer.js | 375 + app/importer-csv/server/index.js | 1 + .../client/adder.js | 4 + .../client/index.js | 1 + app/importer-hipchat-enterprise/lib/info.js | 12 + .../server/adder.js | 5 + .../server/importer.js | 1333 + .../server/index.js | 1 + app/importer-hipchat/client/adder.js | 4 + app/importer-hipchat/client/index.js | 1 + app/importer-hipchat/lib/info.js | 7 + app/importer-hipchat/server/adder.js | 5 + app/importer-hipchat/server/importer.js | 368 + app/importer-hipchat/server/index.js | 1 + app/importer-slack-users/client/adder.js | 4 + app/importer-slack-users/client/index.js | 1 + app/importer-slack-users/lib/info.js | 10 + app/importer-slack-users/server/adder.js | 5 + app/importer-slack-users/server/importer.js | 148 + app/importer-slack-users/server/index.js | 1 + app/importer-slack/client/adder.js | 4 + app/importer-slack/client/index.js | 1 + app/importer-slack/lib/info.js | 7 + app/importer-slack/server/adder.js | 5 + app/importer-slack/server/importer.js | 560 + app/importer-slack/server/index.js | 1 + .../client/ImporterWebsocketReceiver.js | 6 +- app/importer/client/admin/adminImport.html | 38 + app/importer/client/admin/adminImport.js | 51 + .../client/admin/adminImportHistory.html | 135 + .../client/admin/adminImportHistory.js | 206 + .../client/admin/adminImportPrepare.html | 100 + .../client/admin/adminImportPrepare.js | 324 + .../client/admin/adminImportProgress.html | 7 + .../client/admin/adminImportProgress.js | 9 +- app/importer/client/index.js | 19 + .../importer}/lib/ImporterInfo.js | 0 .../importer}/lib/ImporterProgressStep.js | 3 + .../importer}/lib/Importers.js | 0 app/importer/server/classes/ImporterBase.js | 438 + .../server/classes/ImporterProgress.js | 0 .../server/classes/ImporterSelection.js | 0 .../classes/ImporterSelectionChannel.js | 4 +- .../server/classes/ImporterSelectionUser.js | 4 +- .../server/classes/ImporterWebsocket.js | 2 + app/importer/server/index.js | 37 + .../methods/downloadPublicImportFile.js | 75 + .../server/methods/getImportFileData.js | 84 + .../server/methods/getImportProgress.js | 8 +- .../methods/getLatestImportOperations.js | 25 + .../server/methods/getSelectionData.js | 8 +- .../importer}/server/methods/prepareImport.js | 12 +- app/importer/server/methods/restartImport.js | 37 + .../importer}/server/methods/setupImporter.js | 8 +- .../importer}/server/methods/startImport.js | 8 +- .../server/methods/uploadImportFile.js | 38 + app/importer/server/models/Imports.js | 32 + app/importer/server/models/RawImports.js | 9 + .../server/startup/setImportsToInvalid.js | 2 + app/importer/server/startup/store.js | 22 + app/integrations/client/collections.js | 4 + app/integrations/client/index.js | 15 + app/integrations/client/route.js | 77 + app/integrations/client/startup.js | 9 + .../client/stylesheets/integrations.css | 4 - .../client/views/additional/zapier.html | 0 .../client/views/integrations.html | 2 +- app/integrations/client/views/integrations.js | 31 + .../client/views/integrationsIncoming.html | 6 +- .../client/views/integrationsIncoming.js | 79 +- .../client/views/integrationsNew.html | 2 +- .../client/views/integrationsNew.js | 18 + .../client/views/integrationsOutgoing.html | 8 +- .../client/views/integrationsOutgoing.js | 78 +- .../views/integrationsOutgoingHistory.html | 2 +- .../views/integrationsOutgoingHistory.js | 40 +- .../client/views/messageExample.js | 39 + app/integrations/lib/rocketchat.js | 67 + app/integrations/server/api/api.js | 411 + app/integrations/server/index.js | 16 + .../server/lib/triggerHandler.js | 61 +- .../integrations}/server/lib/validation.js | 31 +- app/integrations/server/logger.js | 8 + .../server/methods/clearIntegrationHistory.js | 26 + .../incoming/addIncomingIntegration.js | 105 + .../incoming/deleteIncomingIntegration.js | 26 + .../incoming/updateIncomingIntegration.js | 112 + .../outgoing/addOutgoingIntegration.js | 24 + .../outgoing/deleteOutgoingIntegration.js | 27 + .../outgoing/replayOutgoingIntegration.js | 33 + .../outgoing/updateOutgoingIntegration.js | 61 + .../server/publications/integrationHistory.js | 17 + .../server/publications/integrations.js | 17 + app/integrations/server/triggers.js | 17 + .../rocketchat-irc => app/irc}/README.md | 0 app/irc/index.js | 1 + app/irc/server/index.js | 3 + app/irc/server/irc-bridge/index.js | 140 + .../server/irc-bridge/localHandlers/index.js | 0 .../irc-bridge/localHandlers/onCreateRoom.js | 13 + .../irc-bridge/localHandlers/onCreateUser.js | 8 +- .../irc-bridge/localHandlers/onJoinRoom.js | 0 .../irc-bridge/localHandlers/onLeaveRoom.js | 0 .../irc-bridge/localHandlers/onLogin.js | 8 +- .../irc-bridge/localHandlers/onLogout.js | 0 .../irc-bridge/localHandlers/onSaveMessage.js | 8 +- .../irc-bridge/peerHandlers/disconnected.js | 17 + .../server/irc-bridge/peerHandlers/index.js | 0 .../irc-bridge/peerHandlers/joinedChannel.js | 25 + .../irc-bridge/peerHandlers/leftChannel.js | 21 + .../irc-bridge/peerHandlers/nickChanged.js | 6 +- .../irc-bridge/peerHandlers/sentMessage.js | 81 + .../irc-bridge/peerHandlers/userRegistered.js | 8 +- .../irc}/server/irc-settings.js | 6 +- app/irc/server/irc.js | 27 + app/irc/server/methods/resetIrcConnection.js | 56 + .../irc}/server/servers/RFC2813/codes.js | 0 app/irc/server/servers/RFC2813/index.js | 181 + .../servers/RFC2813/localCommandHandlers.js | 15 +- .../server/servers/RFC2813/parseMessage.js | 2 +- .../servers/RFC2813/peerCommandHandlers.js | 0 .../irc}/server/servers/index.js | 0 app/issuelinks/client/client.js | 21 + app/issuelinks/client/index.js | 1 + app/issuelinks/server/index.js | 1 + app/issuelinks/server/settings.js | 19 + app/katex/.eslintrc | 5 + app/katex/client/index.js | 1 + .../katex}/client/style.css | 0 app/katex/katex.min.css | 1 + app/katex/lib/katex.js | 194 + app/katex/server/index.js | 3 + app/katex/server/settings.js | 36 + app/lazy-load/client/index.js | 43 + .../lazy-load}/client/lazyloadImage.html | 0 app/lazy-load/client/lazyloadImage.js | 38 + app/lazy-load/index.js | 1 + app/ldap/client/index.js | 1 + app/ldap/client/loginHelper.js | 42 + .../ldap}/server/index.js | 0 .../ldap}/server/ldap.js | 71 +- app/ldap/server/loginHandler.js | 158 + app/ldap/server/settings.js | 96 + .../ldap}/server/sync.js | 88 +- app/ldap/server/syncUsers.js | 31 + .../ldap}/server/testConnection.js | 8 +- app/lib/README.md | 159 + app/lib/client/CustomTranslations.js | 14 + app/lib/client/OAuthProxy.js | 16 + app/lib/client/UserDeleted.js | 12 + app/lib/client/defaultTabBars.js | 72 + app/lib/client/index.js | 15 + .../lib}/client/lib/LoginPresence.js | 6 +- .../lib}/client/lib/RocketChatAnnouncement.js | 10 + app/lib/client/lib/formatDate.js | 53 + app/lib/client/lib/index.js | 10 + app/lib/client/lib/settings.js | 48 + app/lib/client/lib/startup/commands.js | 22 + .../lib}/client/lib/userRoles.js | 9 +- app/lib/client/methods/sendMessage.js | 40 + .../lib}/client/views/customFieldsForm.html | 0 .../lib}/client/views/customFieldsForm.js | 12 +- app/lib/index.js | 8 + app/lib/lib/MessageTypes.js | 162 + app/lib/lib/roomTypes/conversation.js | 18 + app/lib/lib/roomTypes/direct.js | 166 + app/lib/lib/roomTypes/favorite.js | 20 + .../lib}/lib/roomTypes/index.js | 0 app/lib/lib/roomTypes/private.js | 133 + app/lib/lib/roomTypes/public.js | 128 + app/lib/lib/roomTypes/unread.js | 19 + app/lib/lib/startup/settingsOnLoadSiteUrl.js | 32 + .../functions/addUserToDefaultChannels.js | 25 + app/lib/server/functions/addUserToRoom.js | 60 + app/lib/server/functions/archiveRoom.js | 11 + app/lib/server/functions/attachMessage.js | 15 + .../functions/checkEmailAvailability.js | 6 + .../functions/checkUsernameAvailability.js | 27 + app/lib/server/functions/cleanRoomHistory.js | 52 + app/lib/server/functions/createRoom.js | 179 + app/lib/server/functions/deleteMessage.js | 61 + app/lib/server/functions/deleteRoom.js | 10 + app/lib/server/functions/deleteUser.js | 105 + .../functions/getAvatarSuggestionForUser.js | 124 + app/lib/server/functions/getFullUserData.js | 94 + .../getRoomByNameOrIdWithOptionToJoin.js | 82 + app/lib/server/functions/getStatusText.js | 19 + .../server/functions/getUsernameSuggestion.js | 77 + app/lib/server/functions/index.js | 33 + app/lib/server/functions/insertMessage.js | 144 + app/lib/server/functions/isTheLastMessage.js | 3 + .../server/functions/loadMessageHistory.js | 71 + .../server/functions/notifications/audio.js | 43 + .../server/functions/notifications/desktop.js | 74 + .../server/functions/notifications/email.js | 164 + .../server/functions/notifications/index.js | 74 + .../server/functions/notifications/mobile.js | 109 + .../server/functions/processWebhookMessage.js | 89 + .../server/functions/removeUserFromRoom.js | 36 + app/lib/server/functions/saveCustomFields.js | 12 + .../saveCustomFieldsWithoutValidation.js | 41 + app/lib/server/functions/saveUser.js | 297 + app/lib/server/functions/sendMessage.js | 226 + app/lib/server/functions/setEmail.js | 45 + app/lib/server/functions/setRealName.js | 50 + app/lib/server/functions/setStatusText.js | 53 + .../lib}/server/functions/setUserAvatar.js | 20 +- app/lib/server/functions/setUsername.js | 103 + app/lib/server/functions/unarchiveRoom.js | 6 + app/lib/server/functions/updateMessage.js | 63 + .../server/functions/validateCustomFields.js | 9 +- app/lib/server/index.js | 70 + .../lib}/server/lib/PasswordPolicyClass.js | 2 + app/lib/server/lib/RateLimiter.js | 43 + app/lib/server/lib/bugsnag.js | 35 + app/lib/server/lib/configLogger.js | 19 + app/lib/server/lib/debug.js | 98 + .../server/lib/defaultBlockedDomainsList.js | 3 +- app/lib/server/lib/index.js | 17 + .../server/lib/interceptDirectReplyEmails.js | 46 +- .../server/lib/loginErrorMessageOverride.js | 3 + app/lib/server/lib/meteorFixes.js | 74 + app/lib/server/lib/msgStream.js | 38 + app/lib/server/lib/notifyUsersOnMessage.js | 118 + app/lib/server/lib/passwordPolicy.js | 14 + app/lib/server/lib/processDirectEmail.js | 135 + .../server/lib/sendNotificationsOnMessage.js | 356 + app/lib/server/lib/validateEmailDomain.js | 49 + app/lib/server/methods/addOAuthService.js | 45 + app/lib/server/methods/addUserToRoom.js | 10 + app/lib/server/methods/addUsersToRoom.js | 90 + app/lib/server/methods/archiveRoom.js | 32 + app/lib/server/methods/blockUser.js | 26 + .../methods/checkRegistrationSecretURL.js | 12 + .../methods/checkUsernameAvailability.js | 31 + app/lib/server/methods/cleanRoomHistory.js | 30 + app/lib/server/methods/createChannel.js | 21 + app/lib/server/methods/createPrivateGroup.js | 33 + app/lib/server/methods/createToken.js | 18 + app/lib/server/methods/deleteMessage.js | 62 + .../server/methods/deleteUserOwnAccount.js | 47 + .../methods/executeSlashCommandPreview.js | 34 + app/lib/server/methods/filterATAllTag.js | 46 + app/lib/server/methods/filterATHereTag.js | 45 + app/lib/server/methods/filterBadWords.js | 22 + app/lib/server/methods/getChannelHistory.js | 88 + app/lib/server/methods/getFullUserData.js | 10 + app/lib/server/methods/getMessages.js | 32 + app/lib/server/methods/getRoomJoinCode.js | 23 + app/lib/server/methods/getRoomRoles.js | 43 + app/lib/server/methods/getServerInfo.js | 9 + app/lib/server/methods/getSingleMessage.js | 22 + .../server/methods/getSlashCommandPreviews.js | 28 + app/lib/server/methods/getUserRoles.js | 33 + .../server/methods/getUsernameSuggestion.js | 17 + app/lib/server/methods/insertOrUpdateUser.js | 16 + app/lib/server/methods/joinDefaultChannels.js | 15 + app/lib/server/methods/joinRoom.js | 42 + app/lib/server/methods/leaveRoom.js | 38 + app/lib/server/methods/refreshOAuthService.js | 21 + app/lib/server/methods/removeOAuthService.js | 44 + .../lib}/server/methods/restartServer.js | 6 +- app/lib/server/methods/robotMethods.js | 32 + app/lib/server/methods/saveSetting.js | 48 + app/lib/server/methods/saveSettings.js | 54 + app/lib/server/methods/sendInvitationEmail.js | 51 + app/lib/server/methods/sendMessage.js | 108 + app/lib/server/methods/sendSMTPTestEmail.js | 46 + app/lib/server/methods/setAdminStatus.js | 26 + app/lib/server/methods/setEmail.js | 36 + app/lib/server/methods/setRealName.js | 30 + app/lib/server/methods/setUsername.js | 62 + app/lib/server/methods/unarchiveRoom.js | 28 + app/lib/server/methods/unblockUser.js | 26 + app/lib/server/methods/updateMessage.js | 61 + app/lib/server/oauth/facebook.js | 69 + .../lib}/server/oauth/google.js | 10 +- app/lib/server/oauth/oauth.js | 55 + app/lib/server/oauth/proxy.js | 12 + .../lib}/server/oauth/twitter.js | 8 +- app/lib/server/publications/settings.js | 111 + app/lib/server/startup/email.js | 430 + app/lib/server/startup/oAuthServicesUpdate.js | 118 + app/lib/server/startup/rateLimiter.js | 208 + app/lib/server/startup/robots.js | 11 + app/lib/server/startup/settings.js | 2755 ++ .../server/startup/settingsOnLoadCdnPrefix.js | 35 + .../startup/settingsOnLoadDirectReply.js | 73 + app/lib/server/startup/settingsOnLoadSMTP.js | 68 + app/lib/startup/defaultRoomTypes.js | 16 + app/lib/startup/index.js | 4 + app/lib/tests/server.mocks.js | 9 + app/lib/tests/server.tests.js | 207 + app/livechat/client/collections/AgentUsers.js | 3 + .../client/collections/LivechatCustomField.js | 3 + .../client/collections/LivechatDepartment.js | 3 + .../collections/LivechatDepartmentAgents.js | 3 + .../client/collections/LivechatIntegration.js | 3 + .../client/collections/LivechatMonitoring.js | 3 + .../client/collections/LivechatQueueUser.js | 3 + .../client/collections/LivechatTrigger.js | 3 + .../client/collections/LivechatVisitor.js | 3 + .../client/collections/livechatOfficeHour.js | 3 + app/livechat/client/index.js | 37 + .../livechat}/client/lib/chartHandler.js | 3 +- .../livechat}/client/lib/dataHandler.js | 18 +- .../livechat}/client/lib/dateHandler.js | 39 +- app/livechat/client/roomType.js | 4 + app/livechat/client/route.js | 168 + .../client/startup/notifyUnreadRooms.js | 49 + .../client/stylesheets/livechat.less | 15 +- app/livechat/client/ui.js | 55 + .../app/analytics/livechatAnalytics.html | 4 +- .../views/app/analytics/livechatAnalytics.js | 12 +- .../livechatAnalyticsCustomDaterange.html | 0 .../livechatAnalyticsCustomDaterange.js | 7 +- .../analytics/livechatAnalyticsDaterange.html | 0 .../analytics/livechatAnalyticsDaterange.js | 8 +- .../analytics/livechatRealTimeMonitoring.html | 4 +- .../analytics/livechatRealTimeMonitoring.js | 11 +- .../livechatIntegrationFacebook.html | 0 .../livechatIntegrationFacebook.js | 8 + .../livechatIntegrationWebhook.html | 64 + .../livechatIntegrationWebhook.js | 9 +- .../client/views/app/livechatAppearance.html | 214 + .../client/views/app/livechatAppearance.js | 413 + .../views/app/livechatAutocompleteUser.html | 20 + .../views/app/livechatAutocompleteUser.js | 109 + .../views/app/livechatCurrentChats.html | 111 + .../client/views/app/livechatCurrentChats.js | 154 + .../views/app/livechatCustomFieldForm.html | 6 +- .../views/app/livechatCustomFieldForm.js | 9 + .../views/app/livechatCustomFields.html | 37 + .../client/views/app/livechatCustomFields.js | 54 + .../client/views/app/livechatDashboard.html | 0 .../views/app/livechatDepartmentForm.html | 108 + .../views/app/livechatDepartmentForm.js | 22 + .../client/views/app/livechatDepartments.html | 34 + .../client/views/app/livechatDepartments.js | 54 + .../views/app/livechatInstallation.html | 18 + .../client/views/app/livechatInstallation.js | 37 + .../views/app/livechatNotSubscribed.html | 0 .../client/views/app/livechatOfficeHours.html | 69 + .../client/views/app/livechatOfficeHours.js | 179 + .../client/views/app/livechatQueue.html | 0 .../client/views/app/livechatQueue.js | 72 + .../client/views/app/livechatReadOnly.html | 16 + .../client/views/app/livechatReadOnly.js | 56 + .../client/views/app/livechatTriggers.html | 33 + .../client/views/app/livechatTriggers.js | 83 + .../views/app/livechatTriggersForm.html | 6 +- .../client/views/app/livechatTriggersForm.js | 8 + .../client/views/app/livechatUsers.html | 146 + .../client/views/app/livechatUsers.js | 234 + .../views/app/tabbar/externalSearch.html | 0 .../client/views/app/tabbar/externalSearch.js | 27 + .../client/views/app/tabbar/visitorEdit.html | 0 .../client/views/app/tabbar/visitorEdit.js | 11 +- .../views/app/tabbar/visitorForward.html | 0 .../client/views/app/tabbar/visitorForward.js | 98 + .../views/app/tabbar/visitorHistory.html | 0 .../client/views/app/tabbar/visitorHistory.js | 50 + .../client/views/app/tabbar/visitorInfo.html | 15 +- .../client/views/app/tabbar/visitorInfo.js | 275 + .../views/app/tabbar/visitorNavigation.html | 0 .../views/app/tabbar/visitorNavigation.js | 7 + .../app/triggers/livechatTriggerAction.html | 0 .../app/triggers/livechatTriggerAction.js | 10 +- .../triggers/livechatTriggerCondition.html | 0 .../app/triggers/livechatTriggerCondition.js | 5 +- .../client/views/sideNav/livechat.html | 0 app/livechat/client/views/sideNav/livechat.js | 128 + .../client/views/sideNav/livechatFlex.html | 0 .../client/views/sideNav/livechatFlex.js | 25 + .../imports/server/rest/departments.js | 110 + app/livechat/imports/server/rest/facebook.js | 96 + app/livechat/imports/server/rest/inquiries.js | 69 + .../livechat}/imports/server/rest/sms.js | 18 +- app/livechat/imports/server/rest/upload.js | 105 + app/livechat/imports/server/rest/users.js | 147 + app/livechat/lib/Assets.js | 29 + app/livechat/lib/LivechatExternalMessage.js | 22 + app/livechat/lib/LivechatInquiry.js | 106 + app/livechat/lib/LivechatRoomType.js | 107 + app/livechat/lib/messageTypes.js | 55 + app/livechat/server/agentStatus.js | 10 + app/livechat/server/api.js | 6 + app/livechat/server/api/lib/livechat.js | 143 + .../livechat}/server/api/rest.js | 0 app/livechat/server/api/v1/agent.js | 78 + app/livechat/server/api/v1/config.js | 39 + app/livechat/server/api/v1/customField.js | 66 + app/livechat/server/api/v1/message.js | 303 + app/livechat/server/api/v1/offlineMessage.js | 27 + app/livechat/server/api/v1/pageVisited.js | 34 + app/livechat/server/api/v1/room.js | 174 + app/livechat/server/api/v1/transcript.js | 26 + app/livechat/server/api/v1/videoCall.js | 53 + app/livechat/server/api/v1/visitor.js | 153 + app/livechat/server/config.js | 449 + app/livechat/server/hooks/RDStation.js | 61 + app/livechat/server/hooks/externalMessage.js | 67 + app/livechat/server/hooks/leadCapture.js | 47 + .../server/hooks/markRoomResponded.js | 29 + app/livechat/server/hooks/offlineMessage.js | 21 + .../server/hooks/saveAnalyticsData.js | 65 + app/livechat/server/hooks/sendToCRM.js | 118 + app/livechat/server/hooks/sendToFacebook.js | 37 + app/livechat/server/index.js | 91 + .../livechat}/server/lib/Analytics.js | 58 +- app/livechat/server/lib/Livechat.js | 978 + app/livechat/server/lib/OfficeClock.js | 15 + app/livechat/server/lib/OmniChannel.js | 70 + app/livechat/server/lib/QueueMethods.js | 204 + app/livechat/server/livechat.js | 43 + app/livechat/server/methods/addAgent.js | 14 + app/livechat/server/methods/addManager.js | 14 + .../server/methods/changeLivechatStatus.js | 21 + app/livechat/server/methods/closeByVisitor.js | 20 + app/livechat/server/methods/closeRoom.js | 27 + app/livechat/server/methods/facebook.js | 66 + app/livechat/server/methods/getAgentData.js | 24 + .../server/methods/getAgentOverviewData.js | 21 + .../server/methods/getAnalyticsChartData.js | 21 + .../methods/getAnalyticsOverviewData.js | 21 + .../server/methods/getCustomFields.js | 9 + .../server/methods/getFirstRoomMessage.js | 23 + app/livechat/server/methods/getInitialData.js | 101 + app/livechat/server/methods/getNextAgent.js | 31 + app/livechat/server/methods/loadHistory.js | 16 + app/livechat/server/methods/loginByToken.js | 17 + app/livechat/server/methods/pageVisited.js | 9 + app/livechat/server/methods/registerGuest.js | 52 + app/livechat/server/methods/removeAgent.js | 14 + .../server/methods/removeCustomField.js | 23 + .../server/methods/removeDepartment.js | 14 + app/livechat/server/methods/removeManager.js | 14 + app/livechat/server/methods/removeRoom.js | 36 + app/livechat/server/methods/removeTrigger.js | 17 + .../server/methods/returnAsInquiry.js | 14 + app/livechat/server/methods/saveAppearance.js | 40 + .../server/methods/saveCustomField.js | 32 + app/livechat/server/methods/saveDepartment.js | 14 + app/livechat/server/methods/saveInfo.js | 46 + .../server/methods/saveIntegration.js | 37 + .../server/methods/saveOfficeHours.js | 9 + .../server/methods/saveSurveyFeedback.js | 10 +- app/livechat/server/methods/saveTrigger.js | 28 + app/livechat/server/methods/searchAgent.js | 25 + .../server/methods/sendFileLivechatMessage.js | 9 +- .../server/methods/sendMessageLivechat.js | 44 + .../server/methods/sendOfflineMessage.js | 25 + app/livechat/server/methods/sendTranscript.js | 22 + app/livechat/server/methods/setCustomField.js | 18 + .../server/methods/setDepartmentForVisitor.js | 30 + .../server/methods/setUpConnection.js | 21 + .../server/methods/startFileUploadRoom.js | 21 + app/livechat/server/methods/startVideoCall.js | 39 + app/livechat/server/methods/takeInquiry.js | 89 + app/livechat/server/methods/transfer.js | 34 + .../livechat}/server/methods/webhookTest.js | 14 +- app/livechat/server/permissions.js | 27 + .../server/publications/customFields.js | 21 + .../server/publications/departmentAgents.js | 16 + .../server/publications/externalMessages.js | 7 + .../server/publications/livechatAgents.js | 33 + .../server/publications/livechatAppearance.js | 56 + .../publications/livechatDepartments.js | 19 + .../server/publications/livechatInquiries.js | 44 + .../publications/livechatIntegration.js | 34 + .../server/publications/livechatManagers.js | 33 + .../server/publications/livechatMonitoring.js | 43 + .../publications/livechatOfficeHours.js | 12 + .../server/publications/livechatQueue.js | 52 + .../server/publications/livechatRooms.js | 12 +- .../server/publications/livechatTriggers.js | 19 + .../server/publications/livechatVisitors.js | 8 +- .../server/publications/visitorHistory.js | 45 + .../server/publications/visitorInfo.js | 21 + .../server/publications/visitorPageVisited.js | 39 + app/livechat/server/roomType.js | 36 + app/livechat/server/sendMessageBySMS.js | 46 + app/livechat/server/startup.js | 46 + app/livechat/server/unclosedLivechats.js | 95 + app/livechat/server/visitorStatus.js | 12 + .../livestream}/.gitignore | 0 app/livestream/client/index.js | 9 + app/livestream/client/oauth.js | 17 + .../client/styles/liveStreamTab.css | 0 app/livestream/client/tabBar.js | 26 + .../client/views/broadcastView.html | 0 .../livestream}/client/views/broadcastView.js | 33 +- .../client/views/liveStreamTab.html | 0 .../livestream}/client/views/liveStreamTab.js | 44 +- .../client/views/liveStreamView.html | 0 .../client/views/liveStreamView.js | 2 + .../client/views/livestreamBroadcast.html | 0 .../client/views/livestreamBroadcast.js | 0 .../server/functions/livestream.js | 27 +- app/livestream/server/index.js | 3 + app/livestream/server/methods.js | 140 + app/livestream/server/routes.js | 53 + app/livestream/server/settings.js | 25 + .../logger}/README.md | 0 app/logger/client/ansispan.js | 34 + app/logger/client/index.js | 3 + app/logger/client/logger.js | 86 + app/logger/client/viewLogs.js | 33 + app/logger/client/views/viewLogs.css | 29 + app/logger/client/views/viewLogs.html | 17 + app/logger/client/views/viewLogs.js | 114 + app/logger/index.js | 8 + app/logger/server/index.js | 7 + app/logger/server/server.js | 396 + app/mail-messages/client/index.js | 6 + app/mail-messages/client/router.js | 20 + app/mail-messages/client/startup.js | 11 + .../mail-messages}/client/views/mailer.html | 0 app/mail-messages/client/views/mailer.js | 47 + .../client/views/mailerUnsubscribe.html | 0 .../client/views/mailerUnsubscribe.js | 5 + .../server/functions/sendMail.js | 66 + .../server/functions/unsubscribe.js | 8 + app/mail-messages/server/index.js | 8 + app/mail-messages/server/lib/Mailer.js | 7 + app/mail-messages/server/methods/sendMail.js | 29 + .../server/methods/unsubscribe.js | 18 + app/mail-messages/server/startup.js | 12 + app/mailer/index.js | 1 + app/mailer/server/api.js | 116 + app/mapview/client/index.js | 1 + .../mapview}/client/mapview.js | 10 +- app/mapview/server/index.js | 1 + app/mapview/server/settings.js | 8 + app/markdown/client/index.js | 1 + app/markdown/lib/markdown.js | 101 + app/markdown/lib/parser/marked/marked.js | 115 + .../markdown/lib}/parser/original/code.js | 13 +- app/markdown/lib/parser/original/markdown.js | 96 + .../markdown/lib}/parser/original/original.js | 0 app/markdown/server/index.js | 3 + app/markdown/server/settings.js | 89 + app/markdown/tests/client.mocks.js | 61 + app/markdown/tests/client.tests.js | 293 + app/mentions-flextab/client/actionButton.js | 23 + app/mentions-flextab/client/index.js | 4 + .../client/lib/MentionedMessage.js | 3 + app/mentions-flextab/client/tabBar.js | 14 + .../client/views/mentionsFlexTab.html | 21 + .../client/views/mentionsFlexTab.js | 47 + app/mentions-flextab/server/index.js | 1 + .../server/publications/mentionedMessages.js | 39 + app/mentions/client/client.js | 28 + app/mentions/client/index.js | 2 + app/mentions/client/mentionLink.css | 37 + app/mentions/lib/MentionsParser.js | 109 + app/mentions/server/Mentions.js | 87 + app/mentions/server/index.js | 2 + .../methods/getUserMentionsByChannel.js | 24 + app/mentions/server/server.js | 39 + app/mentions/tests/client.tests.js | 371 + app/mentions/tests/server.tests.js | 266 + app/message-action/client/index.js | 2 + .../message-action}/client/messageAction.html | 4 +- app/message-action/client/messageAction.js | 13 + .../client/stylesheets/messageAction.css | 0 app/message-action/index.js | 1 + app/message-attachments/client/index.js | 9 + .../client/messageAttachment.html | 177 + .../client/messageAttachment.js | 82 + .../client/renderField.html | 20 + app/message-attachments/client/renderField.js | 56 + .../client/stylesheets/messageAttachments.css | 27 +- app/message-attachments/index.js | 1 + .../client/actionButton.js | 37 + app/message-mark-as-unread/client/index.js | 1 + app/message-mark-as-unread/server/index.js | 1 + app/message-mark-as-unread/server/logger.js | 9 + .../server/unreadMessages.js | 49 + app/message-pin/client/actionButton.js | 99 + app/message-pin/client/index.js | 6 + app/message-pin/client/lib/PinnedMessage.js | 3 + app/message-pin/client/messageType.js | 11 + app/message-pin/client/pinMessage.js | 43 + app/message-pin/client/tabBar.js | 22 + .../client/views/pinnedMessages.html | 22 + .../client/views/pinnedMessages.js | 53 + .../client/views/stylesheets/messagepin.css | 0 app/message-pin/server/index.js | 4 + app/message-pin/server/pinMessage.js | 164 + .../server/publications/pinnedMessages.js | 33 + app/message-pin/server/settings.js | 17 + app/message-pin/server/startup/indexes.js | 13 + app/message-snippet/client/actionButton.js | 69 + app/message-snippet/client/index.js | 9 + app/message-snippet/client/lib/collections.js | 3 + app/message-snippet/client/messageType.js | 16 + .../client/page/snippetPage.html | 0 .../client/page/snippetPage.js | 43 + .../client/page/stylesheets/snippetPage.css | 0 app/message-snippet/client/router.js | 19 + app/message-snippet/client/snippetMessage.js | 30 + app/message-snippet/client/tabBar/tabBar.js | 22 + .../tabBar/views/snippetedMessages.html | 24 + .../client/tabBar/views/snippetedMessages.js | 37 + app/message-snippet/server/index.js | 5 + .../server/methods/snippetMessage.js | 54 + .../server/publications/snippetedMessage.js | 55 + .../publications/snippetedMessagesByRoom.js | 44 + app/message-snippet/server/requests.js | 65 + .../server/startup/settings.js | 17 + app/message-star/client/actionButton.js | 106 + app/message-star/client/index.js | 5 + app/message-star/client/lib/StarredMessage.js | 3 + app/message-star/client/starMessage.js | 25 + app/message-star/client/tabBar.js | 14 + .../client/views/starredMessages.html | 21 + .../client/views/starredMessages.js | 52 + .../client/views/stylesheets/messagestar.css | 0 app/message-star/server/index.js | 4 + .../server/publications/starredMessages.js | 37 + app/message-star/server/settings.js | 11 + app/message-star/server/starMessage.js | 33 + app/message-star/server/startup/indexes.js | 13 + .../meteor-accounts-saml/CHANGELOG.md | 0 .../meteor-accounts-saml/README.md | 0 app/meteor-accounts-saml/client/index.js | 1 + .../client/saml_client.js | 123 + app/meteor-accounts-saml/server/index.js | 2 + .../server/saml_rocketchat.js | 237 + .../server/saml_server.js | 490 + app/meteor-accounts-saml/server/saml_utils.js | 658 + app/metrics/index.js | 1 + app/metrics/server/callbacksMetrics.js | 34 + app/metrics/server/index.js | 9 + app/metrics/server/lib/metrics.js | 183 + .../metrics/server/lib}/statsTracker.js | 8 +- app/migrations/index.js | 1 + app/migrations/server/index.js | 3 + .../migrations/server}/migrations.js | 45 +- app/models/client/index.js | 58 + app/models/client/models/Avatars.js | 10 + app/models/client/models/CachedChannelList.js | 3 + app/models/client/models/CachedChatRoom.js | 3 + .../client/models/CachedChatSubscription.js | 3 + app/models/client/models/CachedUserList.js | 3 + app/models/client/models/ChatMessage.js | 11 + app/models/client/models/ChatPermissions.js | 8 + app/models/client/models/ChatRoom.js | 11 + app/models/client/models/ChatSubscription.js | 3 + app/models/client/models/CustomSounds.js | 10 + app/models/client/models/CustomUserStatus.js | 10 + app/models/client/models/EmojiCustom.js | 22 + app/models/client/models/FullUser.js | 10 + app/models/client/models/Roles.js | 26 + app/models/client/models/RoomRoles.js | 3 + app/models/client/models/Subscriptions.js | 46 + app/models/client/models/Uploads.js | 10 + app/models/client/models/UserAndRoom.js | 3 + app/models/client/models/UserDataFiles.js | 10 + app/models/client/models/UserRoles.js | 3 + app/models/client/models/Users.js | 37 + app/models/client/models/WebdavAccounts.js | 3 + app/models/client/models/_Base.js | 54 + app/models/index.js | 8 + app/models/server/index.js | 79 + app/models/server/models/Avatars.js | 113 + .../models}/server/models/CredentialTokens.js | 8 +- app/models/server/models/CustomSounds.js | 56 + app/models/server/models/CustomUserStatus.js | 66 + app/models/server/models/EmojiCustom.js | 91 + .../models}/server/models/ExportOperations.js | 21 +- .../server/models/FederationDNSCache.js | 13 + app/models/server/models/FederationEvents.js | 271 + app/models/server/models/FederationKeys.js | 69 + app/models/server/models/FederationPeers.js | 60 + .../server/models/IntegrationHistory.js | 10 +- app/models/server/models/Integrations.js | 29 + .../server/models/LivechatCustomField.js | 47 + .../server/models/LivechatDepartment.js | 98 + .../server/models/LivechatDepartmentAgents.js | 131 + .../server/models/LivechatOfficeHour.js | 21 +- .../server/models/LivechatPageVisited.js | 48 + app/models/server/models/LivechatTrigger.js | 32 + .../models}/server/models/LivechatVisitors.js | 25 +- app/models/server/models/Messages.js | 1120 + app/models/server/models/OAuthApps.js | 9 + .../models}/server/models/OEmbedCache.js | 9 +- app/models/server/models/Permissions.js | 30 + app/models/server/models/ReadReceipts.js | 21 + .../models}/server/models/Reports.js | 9 +- app/models/server/models/Roles.js | 103 + app/models/server/models/Rooms.js | 1422 + app/models/server/models/Sessions.js | 575 + app/models/server/models/Sessions.mocks.js | 7 + app/models/server/models/Sessions.tests.js | 824 + app/models/server/models/Settings.js | 191 + app/models/server/models/SmarshHistory.js | 9 + .../models}/server/models/Statistics.js | 8 +- app/models/server/models/Subscriptions.js | 1334 + app/models/server/models/Uploads.js | 113 + app/models/server/models/UserDataFiles.js | 44 + app/models/server/models/Users.js | 1129 + app/models/server/models/WebdavAccounts.js | 27 + app/models/server/models/_Base.js | 344 + app/models/server/models/_BaseDb.js | 325 + app/models/server/models/apps-logs-model.js | 7 + app/models/server/models/apps-model.js | 7 + .../server/models/apps-persistence-model.js | 7 + app/models/server/raw/BaseRaw.js | 17 + app/models/server/raw/Permissions.js | 4 + app/models/server/raw/Roles.js | 25 + app/models/server/raw/Rooms.js | 26 + app/models/server/raw/Settings.js | 9 + app/models/server/raw/Subscriptions.js | 26 + app/models/server/raw/Users.js | 26 + app/models/server/raw/index.js | 19 + app/notifications/client/index.js | 5 + app/notifications/client/lib/Notifications.js | 99 + app/notifications/index.js | 8 + app/notifications/server/index.js | 5 + app/notifications/server/lib/Notifications.js | 209 + .../rocketchat-nrr => app/nrr}/README.md | 0 app/nrr/client/index.js | 1 + .../rocketchat-nrr => app/nrr/client}/nrr.js | 29 +- app/nrr/index.js | 1 + .../oauth2-server-config}/.gitignore | 0 .../client/admin/collection.js | 3 + .../client/admin/route.js | 26 + .../client/admin/startup.js | 11 + .../client/admin}/views/oauthApp.html | 2 +- .../client/admin}/views/oauthApp.js | 25 +- .../client/admin/views/oauthApps.html | 38 + .../client/admin/views/oauthApps.js | 30 + app/oauth2-server-config/client/index.js | 8 + .../client/oauth}/oauth2-client.html | 0 .../client/oauth}/oauth2-client.js | 12 +- .../client/oauth}/stylesheets/oauth2.css | 0 .../server/admin/functions/parseUriList.js | 17 + .../server/admin/methods/addOAuthApp.js | 37 + .../server/admin/methods/deleteOAuthApp.js | 18 + .../server/admin/methods/updateOAuthApp.js | 48 + .../server/admin/publications/oauthApps.js | 14 + app/oauth2-server-config/server/index.js | 6 + .../server/oauth/default-services.js | 17 + .../server/oauth}/oauth2-server.js | 20 +- .../oembed}/client/baseWidget.html | 0 .../oembed}/client/baseWidget.js | 5 +- app/oembed/client/index.js | 14 + .../oembed}/client/oembedAudioWidget.html | 2 +- app/oembed/client/oembedAudioWidget.js | 13 + .../oembed}/client/oembedFrameWidget.html | 2 +- app/oembed/client/oembedFrameWidget.js | 13 + .../oembed}/client/oembedImageWidget.html | 0 app/oembed/client/oembedImageWidget.js | 22 + .../oembed}/client/oembedUrlWidget.html | 2 +- .../oembed}/client/oembedUrlWidget.js | 9 +- .../oembed}/client/oembedVideoWidget.html | 2 +- app/oembed/client/oembedVideoWidget.js | 38 + .../oembed}/client/oembedYoutubeWidget.html | 4 +- app/oembed/client/oembedYoutubeWidget.js | 14 + app/oembed/server/index.js | 7 + app/oembed/server/jumpToMessage.js | 57 + app/oembed/server/providers.js | 121 + app/oembed/server/server.js | 295 + app/otr/client/index.js | 5 + app/otr/client/rocketchat.otr.js | 108 + .../otr}/client/rocketchat.otr.room.js | 39 +- .../otr}/client/stylesheets/otr.css | 0 app/otr/client/tabBar.js | 26 + .../otr}/client/views/otrFlexTab.html | 0 app/otr/client/views/otrFlexTab.js | 86 + app/otr/server/index.js | 3 + .../server/methods/deleteOldOTRMessages.js | 19 + app/otr/server/methods/updateOTRAck.js | 12 + app/otr/server/settings.js | 9 + app/promises/client/index.js | 5 + app/promises/lib/promises.js | 84 + app/promises/server/index.js | 5 + app/push-notifications/client/index.js | 3 + .../client/stylesheets/pushNotifications.css | 0 app/push-notifications/client/tabBar.js | 14 + .../views/pushNotificationsFlexTab.html | 2 +- .../client/views/pushNotificationsFlexTab.js | 64 +- app/push-notifications/server/index.js | 6 + .../server/lib/PushNotification.js | 59 + .../methods/saveNotificationSettings.js | 108 + .../reactions}/README.md | 0 app/reactions/client/index.js | 2 + app/reactions/client/init.js | 99 + app/reactions/client/methods/setReaction.js | 71 + .../client/stylesheets/reaction.css | 1 - app/reactions/server/index.js | 1 + app/reactions/server/setReaction.js | 113 + .../retention-policy}/README.md | 0 app/retention-policy/index.js | 1 + .../server/cronPruneMessages.js | 132 + app/retention-policy/server/index.js | 2 + .../server/startup/settings.js | 108 + .../search}/README.md | 0 app/search/client/index.js | 5 + app/search/client/provider/result.html | 31 + app/search/client/provider/result.js | 106 + .../search}/client/provider/suggestion.html | 0 .../search}/client/search/search.html | 0 .../search}/client/search/search.js | 22 +- .../search}/client/style/style.css | 1 + app/search/server/events/events.js | 64 + app/search/server/index.js | 11 + app/search/server/logger/logger.js | 4 + app/search/server/model/provider.js | 173 + .../server/provider/defaultProvider.js | 5 +- .../search}/server/service/providerService.js | 28 +- .../server/service/validationService.js | 9 +- app/settings/client/index.js | 5 + app/settings/client/lib/settings.js | 46 + app/settings/index.js | 8 + app/settings/lib/settings.js | 88 + app/settings/server/functions/settings.js | 303 + app/settings/server/index.js | 6 + app/settings/server/observer.js | 19 + app/settings/server/raw.js | 21 + .../setup-wizard}/client/final.html | 0 app/setup-wizard/client/final.js | 58 + app/setup-wizard/client/index.js | 4 + .../setup-wizard}/client/setupWizard.html | 2 +- app/setup-wizard/client/setupWizard.js | 398 + .../server/getSetupWizardParameters.js | 23 + app/setup-wizard/server/index.js | 1 + .../slackbridge}/README.md | 0 app/slackbridge/client/index.js | 1 + .../client/slackbridge_import.client.js | 12 + app/slackbridge/server/RocketAdapter.js | 525 + app/slackbridge/server/SlackAPI.js | 116 + .../slackbridge}/server/SlackAdapter.js | 322 +- app/slackbridge/server/index.js | 3 + app/slackbridge/server/logger.js | 11 + app/slackbridge/server/settings.js | 96 + app/slackbridge/server/slackbridge.js | 107 + .../server/slackbridge_import.server.js | 73 + .../slackbridge}/tests/manual-tests.txt | 0 app/slashcommand-asciiarts/client/index.js | 5 + .../slashcommand-asciiarts/lib}/gimme.js | 5 +- app/slashcommand-asciiarts/lib/lenny.js | 21 + .../slashcommand-asciiarts/lib}/shrug.js | 5 +- .../slashcommand-asciiarts/lib}/tableflip.js | 5 +- .../slashcommand-asciiarts/lib}/unflip.js | 5 +- app/slashcommand-asciiarts/server/index.js | 5 + .../client/client.js | 6 + app/slashcommands-archiveroom/client/index.js | 1 + app/slashcommands-archiveroom/server/index.js | 1 + .../server/server.js | 76 + app/slashcommands-create/client/client.js | 6 + app/slashcommands-create/client/index.js | 1 + app/slashcommands-create/server/index.js | 1 + app/slashcommands-create/server/server.js | 61 + app/slashcommands-help/index.js | 1 + app/slashcommands-help/server/index.js | 1 + app/slashcommands-help/server/server.js | 54 + app/slashcommands-hide/client/hide.js | 6 + app/slashcommands-hide/client/index.js | 1 + app/slashcommands-hide/server/hide.js | 69 + app/slashcommands-hide/server/index.js | 1 + app/slashcommands-invite/client/client.js | 6 + app/slashcommands-invite/client/index.js | 1 + app/slashcommands-invite/server/index.js | 1 + app/slashcommands-invite/server/server.js | 90 + app/slashcommands-inviteall/client/client.js | 10 + app/slashcommands-inviteall/client/index.js | 1 + app/slashcommands-inviteall/server/index.js | 1 + app/slashcommands-inviteall/server/server.js | 96 + app/slashcommands-join/client/client.js | 12 + app/slashcommands-join/client/index.js | 1 + app/slashcommands-join/server/index.js | 1 + app/slashcommands-join/server/server.js | 45 + app/slashcommands-kick/client/client.js | 12 + app/slashcommands-kick/client/index.js | 1 + app/slashcommands-kick/server/index.js | 1 + app/slashcommands-kick/server/server.js | 55 + app/slashcommands-leave/index.js | 1 + app/slashcommands-leave/server/index.js | 1 + app/slashcommands-leave/server/leave.js | 30 + app/slashcommands-me/index.js | 1 + app/slashcommands-me/server/index.js | 1 + app/slashcommands-me/server/me.js | 23 + app/slashcommands-msg/index.js | 1 + app/slashcommands-msg/server/index.js | 1 + app/slashcommands-msg/server/server.js | 57 + app/slashcommands-mute/index.js | 1 + app/slashcommands-mute/server/index.js | 2 + app/slashcommands-mute/server/mute.js | 58 + app/slashcommands-mute/server/unmute.js | 55 + app/slashcommands-open/client/client.js | 54 + app/slashcommands-open/client/index.js | 1 + app/slashcommands-open/index.js | 1 + app/slashcommands-status/client/index.js | 1 + app/slashcommands-status/index.js | 8 + app/slashcommands-status/lib/status.js | 43 + app/slashcommands-status/server/index.js | 1 + app/slashcommands-topic/client/index.js | 1 + app/slashcommands-topic/lib/topic.js | 30 + app/slashcommands-topic/server/index.js | 1 + .../client/client.js | 6 + .../client/index.js | 1 + .../server/index.js | 1 + .../server/server.js | 77 + .../slider}/README.md | 0 app/slider/client/index.js | 2 + .../slider/client}/rocketchat-slider.html | 0 .../slider/client}/rocketchat-slider.js | 3 +- app/slider/index.js | 1 + app/smarsh-connector/index.js | 1 + .../server/functions/generateEml.js | 32 +- .../server/functions/sendEmail.js | 37 + app/smarsh-connector/server/index.js | 4 + app/smarsh-connector/server/lib/rocketchat.js | 1 + app/smarsh-connector/server/settings.js | 64 + app/smarsh-connector/server/startup.js | 33 + .../rocketchat-sms => app/sms}/README.md | 0 app/sms/index.js | 1 + app/sms/server/SMS.js | 26 + app/sms/server/index.js | 5 + .../sms/server}/services/twilio.js | 14 +- app/sms/server/services/voxtelesys.js | 83 + app/sms/server/settings.js | 69 + app/spotify/client/index.js | 3 + .../lib/client/oembedSpotifyWidget.html | 2 +- .../spotify}/lib/client/widget.js | 5 +- .../spotify}/lib/spotify.js | 15 +- app/spotify/server/index.js | 1 + app/statistics/index.js | 1 + app/statistics/server/functions/get.js | 166 + app/statistics/server/functions/save.js | 9 + app/statistics/server/index.js | 6 + app/statistics/server/lib/SAUMonitor.js | 350 + app/statistics/server/lib/UAParserCustom.js | 140 + .../server/lib/UAParserCustom.tests.js | 72 + .../server/methods/getStatistics.js | 22 + app/statistics/server/startup/monitor.js | 10 + app/statistics/server/statisticsNamespace.js | 1 + .../client/imports/components/alerts.css | 0 .../client/imports/components/avatar.css | 0 app/theme/client/imports/components/badge.css | 32 + .../theme}/client/imports/components/chip.css | 0 .../imports/components/contextual-bar.css | 378 + .../client/imports/components/emojiPicker.css | 39 +- .../client/imports/components/flex-nav.css | 0 .../client/imports/components/header.css | 537 + .../imports/components/main-content.css | 4 +- .../client/imports/components/memberlist.css | 0 .../client/imports/components/message-box.css | 374 + .../client/imports/components/messages.css | 207 + .../client/imports/components/modal.css | 21 +- .../components/modal/create-channel.css | 66 + .../imports/components/modal/full-modal.css | 0 .../client/imports/components/popout.css | 5 - .../client/imports/components/popover.css | 25 + .../imports/components/setup-wizard.css | 2 +- .../imports/components/sidebar/rooms-list.css | 2 - .../components/sidebar/sidebar-flex.css | 0 .../components/sidebar/sidebar-header.css | 24 +- .../components/sidebar/sidebar-item.css | 46 +- .../imports/components/sidebar/sidebar.css | 2 +- .../imports/components/sidebar/toolbar.css | 2 - .../client/imports/components/slider.css | 0 .../client/imports/components/table.css | 18 + app/theme/client/imports/components/tabs.css | 49 + .../client/imports/components/tooltip.css | 15 +- .../client/imports/components/userInfo.css | 7 + app/theme/client/imports/forms/button.css | 225 + .../theme}/client/imports/forms/checkbox.css | 0 .../theme}/client/imports/forms/input.css | 32 +- .../client/imports/forms/popup-list.css | 8 +- .../client/imports/forms/select-avatar.css | 0 .../theme}/client/imports/forms/select.css | 24 +- .../theme}/client/imports/forms/switch.css | 0 .../theme}/client/imports/forms/tags.css | 4 +- .../client/imports/general/animations.css | 4 + app/theme/client/imports/general/apps.css | 164 + .../theme}/client/imports/general/base.css | 66 +- .../client/imports/general/base_old.css | 1344 +- .../theme}/client/imports/general/forms.css | 2 +- .../theme}/client/imports/general/reset.css | 4 - app/theme/client/imports/general/rtl.css | 560 + .../client/imports/general/typography.css | 0 .../client/imports/general/variables.css | 378 + app/theme/client/index.js | 1 + .../theme}/client/main.css | 1 - .../theme}/client/vendor/fontello/config.json | 0 .../client/vendor/fontello/css/fontello.css | 0 .../theme}/client/vendor/jscolor.js | 0 .../theme}/client/vendor/photoswipe.css | 0 app/theme/server/index.js | 2 + app/theme/server/server.js | 176 + .../theme}/server/variables.js | 20 +- app/threads/README.md | 17 + app/threads/client/flextab/thread.html | 34 + app/threads/client/flextab/thread.js | 196 + app/threads/client/flextab/threadlist.js | 22 + app/threads/client/flextab/threads.html | 40 + app/threads/client/flextab/threads.js | 177 + app/threads/client/index.js | 7 + app/threads/client/messageAction/follow.js | 36 + .../client/messageAction/replyInThread.js | 40 + app/threads/client/messageAction/unfollow.js | 36 + app/threads/client/threads.css | 168 + app/threads/client/upsert.js | 10 + app/threads/server/functions.js | 49 + app/threads/server/hooks/afterReadMessages.js | 19 + .../server/hooks/afterdeletemessage.js | 30 + app/threads/server/hooks/aftersavemessage.js | 67 + app/threads/server/hooks/index.js | 3 + app/threads/server/index.js | 3 + app/threads/server/methods/followMessage.js | 38 + .../server/methods/getThreadMessages.js | 38 + app/threads/server/methods/getThreadsList.js | 28 + app/threads/server/methods/index.js | 4 + app/threads/server/methods/unfollowMessage.js | 38 + app/threads/server/settings.js | 14 + app/token-login/client/index.js | 1 + app/token-login/client/login_token_client.js | 25 + app/token-login/server/index.js | 1 + .../token-login}/server/login_token_server.js | 3 +- .../tokenpass}/README.md | 0 .../tokenpass}/client/channelSettings.css | 0 app/tokenpass/client/index.js | 7 + .../tokenpass}/client/login-button.css | 0 app/tokenpass/client/roomType.js | 23 + app/tokenpass/client/startup.js | 21 + .../tokenpass}/client/styles.css | 0 .../tokenpass}/client/tokenChannelsList.html | 0 app/tokenpass/client/tokenChannelsList.js | 31 + .../client/tokenpassChannelSettings.html | 0 .../client/tokenpassChannelSettings.js | 20 +- app/tokenpass/lib/common.js | 41 + .../tokenpass}/server/Tokenpass.js | 2 +- app/tokenpass/server/cronRemoveUsers.js | 52 + .../getProtectedTokenpassBalances.js | 24 + .../functions/getPublicTokenpassBalances.js | 24 + .../functions/saveRoomTokensMinimumBalance.js | 17 + .../functions/updateUserTokenpassBalances.js | 18 + app/tokenpass/server/index.js | 11 + .../server/methods/findTokenChannels.js | 26 + .../server/methods/getChannelTokenpass.js | 22 + app/tokenpass/server/startup.js | 58 + .../tooltip}/README.md | 0 app/tooltip/client/index.js | 4 + .../tooltip}/client/rocketchat-tooltip.html | 0 .../tooltip}/client/rocketchat-tooltip.js | 7 +- .../tooltip}/client/tooltip.css | 0 app/tooltip/index.js | 1 + .../ui-account}/README.md | 0 .../ui-account}/client/account.html | 0 app/ui-account/client/account.js | 11 + .../ui-account}/client/accountFlex.html | 8 + app/ui-account/client/accountFlex.js | 43 + .../client/accountIntegrations.html | 26 + app/ui-account/client/accountIntegrations.js | 37 + app/ui-account/client/accountPreferences.html | 354 + app/ui-account/client/accountPreferences.js | 344 + .../ui-account}/client/accountProfile.html | 22 +- .../ui-account}/client/accountProfile.js | 110 +- .../ui-account}/client/avatar/avatar.html | 0 app/ui-account/client/avatar/avatar.js | 31 + .../ui-account}/client/avatar/prompt.html | 0 .../ui-account}/client/avatar/prompt.js | 35 +- app/ui-account/client/index.js | 14 + app/ui-account/index.js | 1 + .../ui-admin}/README.md | 0 .../client/SettingsCachedCollection.js | 38 + .../ui-admin}/client/admin.html | 34 +- app/ui-admin/client/admin.js | 642 + .../ui-admin}/client/adminFlex.html | 8 +- app/ui-admin/client/adminFlex.js | 94 + .../ui-admin}/client/adminInfo.html | 69 + .../ui-admin}/client/adminInfo.js | 18 +- app/ui-admin/client/index.js | 20 + .../ui-admin}/client/rooms/adminRoomInfo.html | 12 +- .../ui-admin}/client/rooms/adminRoomInfo.js | 68 +- app/ui-admin/client/rooms/adminRooms.html | 76 + app/ui-admin/client/rooms/adminRooms.js | 187 + .../client/rooms/channelSettingsDefault.html | 0 .../client/rooms/channelSettingsDefault.js | 11 +- .../client/users/adminInviteUser.html | 0 .../ui-admin}/client/users/adminInviteUser.js | 8 +- .../client/users/adminUserChannels.html | 0 .../client/users/adminUserChannels.js | 29 + .../ui-admin}/client/users/adminUserEdit.html | 0 app/ui-admin/client/users/adminUserInfo.html | 9 + app/ui-admin/client/users/adminUsers.html | 101 + app/ui-admin/client/users/adminUsers.js | 166 + app/ui-admin/server/index.js | 1 + .../server/publications/adminRooms.js | 58 + app/ui-cached-collection/client/index.js | 1 + .../client/models/CachedCollection.js | 389 + app/ui-cached-collection/index.js | 1 + .../ui-clean-history}/README.md | 0 app/ui-clean-history/client/index.js | 3 + app/ui-clean-history/client/lib/startup.js | 18 + .../client/views/cleanHistory.html | 13 +- .../client/views/cleanHistory.js | 32 +- .../client/views/stylesheets/cleanHistory.css | 18 +- app/ui-clean-history/index.js | 1 + .../ui-flextab}/README.md | 0 .../ui-flextab}/client/flexTabBar.html | 11 +- app/ui-flextab/client/flexTabBar.js | 248 + app/ui-flextab/client/index.js | 12 + .../ui-flextab}/client/tabs/inviteUsers.html | 6 +- .../ui-flextab}/client/tabs/inviteUsers.js | 49 +- .../client/tabs/keyboardShortcuts.html | 6 + app/ui-flextab/client/tabs/membersList.html | 60 + app/ui-flextab/client/tabs/membersList.js | 339 + .../client/tabs/uploadedFilesList.html | 16 +- .../client/tabs/uploadedFilesList.js | 205 + app/ui-flextab/client/tabs/userActions.js | 513 + app/ui-flextab/client/tabs/userEdit.html | 186 + app/ui-flextab/client/tabs/userEdit.js | 275 + .../ui-flextab}/client/tabs/userInfo.html | 34 +- app/ui-flextab/client/tabs/userInfo.js | 314 + app/ui-flextab/index.js | 1 + .../ui-login}/README.md | 0 app/ui-login/client/index.js | 19 + .../ui-login}/client/login/footer.html | 0 .../ui-login}/client/login/footer.js | 8 +- app/ui-login/client/login/form.html | 150 + app/ui-login/client/login/form.js | 290 + .../ui-login}/client/login/header.html | 0 app/ui-login/client/login/header.js | 13 + .../ui-login}/client/login/layout.html | 0 app/ui-login/client/login/layout.js | 17 + .../ui-login}/client/login/services.html | 0 app/ui-login/client/login/services.js | 95 + .../ui-login}/client/login/social.html | 0 .../ui-login}/client/login/social.js | 0 .../client/reset-password/resetPassword.html | 0 .../client/reset-password/resetPassword.js | 18 +- app/ui-login/client/routes.js | 9 + .../ui-login}/client/username/layout.html | 0 app/ui-login/client/username/layout.js | 13 + .../ui-login}/client/username/username.html | 5 +- .../ui-login}/client/username/username.js | 23 +- app/ui-login/index.js | 1 + app/ui-master/.eslintrc | 5 + .../ui-master}/README.md | 0 .../ui-master}/client/error.html | 0 app/ui-master/client/index.js | 5 + app/ui-master/client/loading/index.js | 2 + app/ui-master/client/loading/loading.css | 52 + .../ui-master/client/loading}/loading.html | 0 .../ui-master}/client/logoLayout.html | 0 app/ui-master/client/main.html | 59 + app/ui-master/client/main.js | 266 + app/ui-master/public/README.md | 17 + app/ui-master/public/generateHTML.js | 76 + app/ui-master/public/generateSprite.js | 85 + app/ui-master/public/icons/Bell-off.svg | 3 + app/ui-master/public/icons/Download.svg | 3 + app/ui-master/public/icons/Eye.svg | 3 + .../public/icons/File-google-drive.svg | 3 + app/ui-master/public/icons/File-keynote.svg | 3 + app/ui-master/public/icons/Files-audio.svg | 3 + app/ui-master/public/icons/Files-video.svg | 3 + app/ui-master/public/icons/Files-zip.svg | 3 + app/ui-master/public/icons/Multiline.svg | 3 + app/ui-master/public/icons/Send-active.svg | 3 + app/ui-master/public/icons/Star-filled.svg | 3 + app/ui-master/public/icons/Video-off.svg | 3 + app/ui-master/public/icons/Volume-disable.svg | 3 + app/ui-master/public/icons/add-reaction.svg | 3 + app/ui-master/public/icons/arrow-down.svg | 3 + app/ui-master/public/icons/at.svg | 3 + app/ui-master/public/icons/back.svg | 3 + app/ui-master/public/icons/ban.svg | 3 + app/ui-master/public/icons/bell.svg | 3 + app/ui-master/public/icons/bold.svg | 3 + app/ui-master/public/icons/book.svg | 3 + app/ui-master/public/icons/calendar.svg | 3 + app/ui-master/public/icons/card.svg | 3 + app/ui-master/public/icons/chat.svg | 3 + app/ui-master/public/icons/check.svg | 3 + .../public/icons/checkmark-circled.svg | 3 + app/ui-master/public/icons/circle-cross.svg | 3 + app/ui-master/public/icons/circle.svg | 3 + .../public/icons/circled-arrow-down.svg | 3 + app/ui-master/public/icons/clip.svg | 3 + app/ui-master/public/icons/clipboard.svg | 3 + app/ui-master/public/icons/clock.svg | 3 + app/ui-master/public/icons/cloud-plus.svg | 3 + app/ui-master/public/icons/code.svg | 3 + app/ui-master/public/icons/cog.svg | 3 + app/ui-master/public/icons/computer.svg | 3 + app/ui-master/public/icons/copy.svg | 3 + app/ui-master/public/icons/cross.svg | 3 + app/ui-master/public/icons/cube.svg | 3 + app/ui-master/public/icons/customize.svg | 3 + app/ui-master/public/icons/discover.svg | 3 + app/ui-master/public/icons/discussion.svg | 3 + app/ui-master/public/icons/edit-rounded.svg | 3 + app/ui-master/public/icons/edit.svg | 3 + app/ui-master/public/icons/emoji.svg | 3 + app/ui-master/public/icons/eraser.svg | 3 + app/ui-master/public/icons/eye-off.svg | 3 + app/ui-master/public/icons/facebook.svg | 3 + app/ui-master/public/icons/file-document.svg | 3 + app/ui-master/public/icons/file-generic.svg | 3 + app/ui-master/public/icons/file-pdf.svg | 3 + app/ui-master/public/icons/file-sheets.svg | 3 + app/ui-master/public/icons/flag.svg | 3 + app/ui-master/public/icons/folder.svg | 3 + app/ui-master/public/icons/github.svg | 3 + app/ui-master/public/icons/gitlab.svg | 3 + app/ui-master/public/icons/google.svg | 3 + app/ui-master/public/icons/hand-pointer.svg | 3 + app/ui-master/public/icons/hashtag.svg | 3 + app/ui-master/public/icons/help.svg | 3 + app/ui-master/public/icons/hubot.svg | 3 + app/ui-master/public/icons/import.svg | 3 + app/ui-master/public/icons/info-circled.svg | 3 + app/ui-master/public/icons/italic.svg | 3 + app/ui-master/public/icons/jump.svg | 3 + app/ui-master/public/icons/key.svg | 3 + app/ui-master/public/icons/keyboard.svg | 3 + app/ui-master/public/icons/language.svg | 3 + app/ui-master/public/icons/linkedin.svg | 3 + app/ui-master/public/icons/list-alt.svg | 3 + app/ui-master/public/icons/list.svg | 3 + app/ui-master/public/icons/livechat.svg | 3 + app/ui-master/public/icons/loading.svg | 3 + app/ui-master/public/icons/lock.svg | 3 + app/ui-master/public/icons/magnifier.svg | 3 + app/ui-master/public/icons/mail.svg | 3 + app/ui-master/public/icons/map-pin.svg | 3 + app/ui-master/public/icons/menu.svg | 3 + app/ui-master/public/icons/message.svg | 3 + app/ui-master/public/icons/mic.svg | 3 + app/ui-master/public/icons/mobile.svg | 3 + app/ui-master/public/icons/modal-warning.svg | 3 + app/ui-master/public/icons/mute.svg | 3 + app/ui-master/public/icons/pause.svg | 3 + app/ui-master/public/icons/permalink.svg | 3 + app/ui-master/public/icons/pin.svg | 3 + app/ui-master/public/icons/play-solid.svg | 3 + app/ui-master/public/icons/play.svg | 3 + app/ui-master/public/icons/plus.svg | 3 + app/ui-master/public/icons/podcast.svg | 3 + app/ui-master/public/icons/post.svg | 3 + app/ui-master/public/icons/queue.svg | 3 + app/ui-master/public/icons/quote.svg | 3 + app/ui-master/public/icons/reload.svg | 3 + app/ui-master/public/icons/reply-directly.svg | 3 + app/ui-master/public/icons/reply.svg | 3 + app/ui-master/public/icons/report.svg | 3 + app/ui-master/public/icons/send.svg | 3 + app/ui-master/public/icons/share.svg | 3 + app/ui-master/public/icons/shield-alt.svg | 3 + app/ui-master/public/icons/shield-check.svg | 3 + app/ui-master/public/icons/shield.svg | 3 + app/ui-master/public/icons/sign-out.svg | 3 + .../public/icons/sort-amount-down.svg | 3 + app/ui-master/public/icons/sort-down.svg | 6 + app/ui-master/public/icons/sort-up.svg | 6 + app/ui-master/public/icons/sort.svg | 3 + app/ui-master/public/icons/star.svg | 3 + app/ui-master/public/icons/strike.svg | 3 + app/ui-master/public/icons/team.svg | 3 + app/ui-master/public/icons/th-list.svg | 3 + app/ui-master/public/icons/thread.svg | 3 + app/ui-master/public/icons/trash.svg | 3 + app/ui-master/public/icons/twitter.svg | 3 + app/ui-master/public/icons/upload.svg | 3 + app/ui-master/public/icons/user-plus.svg | 3 + app/ui-master/public/icons/user-rounded.svg | 3 + app/ui-master/public/icons/user.svg | 3 + app/ui-master/public/icons/video.svg | 3 + app/ui-master/public/icons/volume-mute.svg | 3 + app/ui-master/public/icons/volume.svg | 3 + app/ui-master/public/icons/warning.svg | 3 + app/ui-master/server/index.js | 1 + app/ui-master/server/inject.js | 184 + .../ui-message}/README.md | 0 app/ui-message/client/index.js | 7 + app/ui-message/client/message.html | 178 + app/ui-message/client/message.js | 623 + .../client/messageBox/messageBox.html | 90 + .../client/messageBox/messageBox.js | 483 + .../client/messageBox/messageBoxActions.js | 123 + .../messageBox/messageBoxAudioMessage.html | 22 + .../messageBox/messageBoxAudioMessage.js | 192 + .../client/messageBox/messageBoxAutogrow.js | 90 + .../client/messageBox/messageBoxFormatting.js | 107 + .../messageBox/messageBoxNotSubscribed.html | 26 + .../messageBox/messageBoxNotSubscribed.js | 68 + .../client/messageBox/messageBoxReadOnly.html | 7 + .../client/messageBox/messageBoxReadOnly.js | 10 + .../messageBox/messageBoxReplyPreview.html | 10 + .../messageBox/messageBoxReplyPreview.js | 19 + .../client/messageBox/messageBoxTyping.html | 20 + .../client/messageBox/messageBoxTyping.js | 35 + app/ui-message/client/messageThread.html | 26 + .../client/popup/messagePopup.html | 0 .../ui-message}/client/popup/messagePopup.js | 41 +- .../client/popup/messagePopupChannel.html | 0 .../client/popup/messagePopupChannel.js | 10 + .../client/popup/messagePopupConfig.html | 9 + .../client/popup/messagePopupConfig.js | 453 + .../client/popup/messagePopupEmoji.html | 0 .../client/popup/messagePopupEmoji.js | 9 + .../popup/messagePopupSlashCommand.html | 0 .../messagePopupSlashCommandPreview.html | 0 .../popup/messagePopupSlashCommandPreview.js | 26 +- .../client/popup/messagePopupUser.html | 0 app/ui-message/index.js | 1 + .../ui-sidenav}/README.md | 0 .../ui-sidenav}/client/chatRoomItem.html | 0 app/ui-sidenav/client/chatRoomItem.js | 50 + app/ui-sidenav/client/index.js | 19 + .../ui-sidenav}/client/roomList.html | 0 app/ui-sidenav/client/roomList.js | 182 + app/ui-sidenav/client/sideNav.html | 30 + app/ui-sidenav/client/sideNav.js | 114 + .../ui-sidenav}/client/sidebarHeader.html | 4 +- app/ui-sidenav/client/sidebarHeader.js | 421 + app/ui-sidenav/client/sidebarItem.html | 76 + app/ui-sidenav/client/sidebarItem.js | 219 + .../ui-sidenav}/client/sortlist.html | 9 + app/ui-sidenav/client/sortlist.js | 52 + .../ui-sidenav}/client/toolbar.html | 2 +- app/ui-sidenav/client/toolbar.js | 208 + .../ui-sidenav}/client/userStatus.html | 0 app/ui-sidenav/index.js | 1 + app/ui-utils/client/config.js | 6 + app/ui-utils/client/index.js | 30 + app/ui-utils/client/lib/AccountBox.js | 101 + .../ui-utils/client/lib}/AdminBox.js | 8 +- app/ui-utils/client/lib/ChannelActions.js | 95 + app/ui-utils/client/lib/IframeLogin.js | 111 + app/ui-utils/client/lib/Layout.js | 14 + app/ui-utils/client/lib/MessageAction.js | 351 + .../ui-utils}/client/lib/RocketChatTabBar.js | 13 +- app/ui-utils/client/lib/RoomHistoryManager.js | 340 + app/ui-utils/client/lib/RoomManager.js | 348 + app/ui-utils/client/lib/SideNav.js | 145 + app/ui-utils/client/lib/TabBar.js | 85 + .../ui-utils/client/lib}/alerts.html | 0 .../app => app/ui-utils/client/lib}/alerts.js | 9 +- app/ui-utils/client/lib/avatar.js | 43 + .../ui-utils}/client/lib/callMethod.js | 4 + app/ui-utils/client/lib/fireGlobalEvent.js | 21 + app/ui-utils/client/lib/keyCodes.js | 36 + app/ui-utils/client/lib/mainReady.js | 3 + app/ui-utils/client/lib/menu.js | 235 + app/ui-utils/client/lib/messageArgs.js | 1 + app/ui-utils/client/lib/messageBox.js | 60 + app/ui-utils/client/lib/messageContext.js | 45 + app/ui-utils/client/lib/modal.html | 63 + app/ui-utils/client/lib/modal.js | 191 + app/ui-utils/client/lib/openRoom.js | 119 + .../ui-utils/client/lib}/popout.html | 6 +- .../app => app/ui-utils/client/lib}/popout.js | 13 +- .../ui-utils/client/lib}/popover.html | 9 +- .../ui-utils/client/lib}/popover.js | 71 +- app/ui-utils/client/lib/prependReplies.js | 23 + .../ui-utils}/client/lib/readMessages.js | 57 +- app/ui-utils/client/lib/renderMessageBody.js | 35 + app/ui-utils/client/lib/rtl.js | 7 + app/ui-utils/index.js | 8 + app/ui-utils/lib/Message.js | 35 + app/ui-utils/lib/MessageProperties.js | 17 + app/ui-utils/lib/MessageTypes.js | 19 + app/ui-utils/server/index.js | 3 + app/ui-utils/tests/server.mocks.js | 8 + app/ui-utils/tests/server.tests.js | 23 + .../ui-vrecord}/README.md | 0 app/ui-vrecord/client/VRecDialog.js | 82 + app/ui-vrecord/client/index.js | 7 + .../ui-vrecord}/client/vrecord.css | 0 .../ui-vrecord}/client/vrecord.html | 0 app/ui-vrecord/client/vrecord.js | 50 + app/ui-vrecord/server/index.js | 1 + app/ui-vrecord/server/settings.js | 9 + {packages/rocketchat-ui => app/ui}/README.md | 0 app/ui/client/components/contextualBar.html | 24 + app/ui/client/components/contextualBar.js | 31 + app/ui/client/components/header/header.html | 34 + app/ui/client/components/header/header.js | 20 + .../client/components/header/headerRoom.html | 75 + app/ui/client/components/header/headerRoom.js | 241 + .../ui}/client/components/icon.html | 0 app/ui/client/components/icon.js | 23 + app/ui/client/components/popupList.html | 45 + app/ui/client/components/popupList.js | 43 + app/ui/client/components/selectDropdown.html | 18 + app/ui/client/components/status.html | 15 + app/ui/client/components/status.js | 66 + .../ui}/client/components/table.html | 0 .../ui}/client/components/table.js | 1 + app/ui/client/components/tabs.html | 16 + app/ui/client/components/tabs.js | 32 + app/ui/client/index.js | 70 + .../ui}/client/lib/Modernizr.js | 0 app/ui/client/lib/accounts.js | 17 + app/ui/client/lib/chatMessages.js | 567 + .../ui}/client/lib/codeMirror/codeMirror.js | 0 .../lib/codeMirror/codeMirrorComponent.html | 0 .../lib/codeMirror/codeMirrorComponent.js | 8 +- app/ui/client/lib/collections.js | 14 + .../ui}/client/lib/customEventPolyfill.js | 0 .../ui}/client/lib/fileUpload.js | 50 +- .../ui}/client/lib/iframeCommands.js | 15 +- app/ui/client/lib/menu.js | 22 + app/ui/client/lib/msgTyping.js | 109 + app/ui/client/lib/notification.js | 182 + .../ui}/client/lib/parentTemplate.js | 4 +- app/ui/client/lib/recorderjs/audioEncoder.js | 62 + app/ui/client/lib/recorderjs/audioRecorder.js | 104 + app/ui/client/lib/recorderjs/videoRecorder.js | 121 + .../ui}/client/lib/rocket.js | 27 +- app/ui/client/lib/textarea-cursor.js | 19 + .../client/views/404/invalidSecretURL.html | 0 .../ui}/client/views/404/roomNotFound.html | 0 app/ui/client/views/404/roomNotFound.js | 23 + .../client/views/app/audioNotification.html | 0 .../ui}/client/views/app/burger.html | 0 app/ui/client/views/app/burger.js | 54 + .../ui}/client/views/app/createChannel.html | 22 +- app/ui/client/views/app/createChannel.js | 436 + app/ui/client/views/app/directory.css | 78 + app/ui/client/views/app/directory.html | 169 + app/ui/client/views/app/directory.js | 278 + app/ui/client/views/app/editStatus.css | 45 + app/ui/client/views/app/editStatus.html | 30 + app/ui/client/views/app/editStatus.js | 113 + .../ui}/client/views/app/fullModal.html | 0 app/ui/client/views/app/fullModal.js | 37 + app/ui/client/views/app/helpers.js | 19 + .../ui}/client/views/app/home.html | 0 app/ui/client/views/app/home.js | 12 + .../ui}/client/views/app/notAuthorized.html | 0 .../ui}/client/views/app/pageContainer.html | 0 .../views/app/pageSettingsContainer.html | 0 .../ui}/client/views/app/photoswipe.html | 0 .../ui}/client/views/app/photoswipe.js | 5 +- app/ui/client/views/app/room.html | 159 + app/ui/client/views/app/room.js | 1278 + .../ui}/client/views/app/roomSearch.html | 0 app/ui/client/views/app/roomSearch.js | 19 + .../ui}/client/views/app/secretURL.html | 0 app/ui/client/views/app/secretURL.js | 37 + .../client/views/app/tests/helpers.tests.js | 1 + .../ui}/client/views/app/userSearch.html | 0 .../views/app/videoCall/videoButtons.html | 0 .../views/app/videoCall/videoButtons.js | 6 +- .../client/views/app/videoCall/videoCall.html | 67 + .../client/views/app/videoCall/videoCall.js | 140 + app/ui/client/views/cmsPage.html | 14 + app/ui/client/views/cmsPage.js | 33 + .../ui}/client/views/fxos.html | 0 .../ui}/client/views/fxos.js | 4 +- .../ui}/client/views/modal.html | 0 app/ui/client/views/modal.js | 3 + app/ui/index.js | 1 + app/user-data-download/index.js | 1 + .../server/cronProcessDownloads.js | 104 +- app/user-data-download/server/index.js | 2 + .../server/startup/settings.js | 33 + .../client/admin/adminUserStatus.html | 72 + .../client/admin/adminUserStatus.js | 137 + .../client/admin/adminUserStatusEdit.html | 7 + .../client/admin/adminUserStatusInfo.html | 7 + app/user-status/client/admin/route.js | 9 + app/user-status/client/admin/startup.js | 11 + .../client/admin/userStatusEdit.html | 42 + .../client/admin/userStatusEdit.js | 115 + .../client/admin/userStatusInfo.html | 22 + .../client/admin/userStatusInfo.js | 117 + .../client/admin/userStatusPreview.html | 5 + app/user-status/client/index.js | 17 + .../client/lib/customUserStatus.js | 61 + app/user-status/client/lib/userStatus.js | 36 + .../notifications/deleteCustomUserStatus.js | 8 + .../notifications/updateCustomUserStatus.js | 8 + app/user-status/index.js | 8 + app/user-status/server/index.js | 7 + .../server/methods/deleteCustomUserStatus.js | 26 + .../server/methods/getUserStatusText.js | 14 + .../methods/insertOrUpdateUserStatus.js | 70 + .../server/methods/listCustomUserStatus.js | 14 + .../server/methods/setUserStatus.js | 34 + .../server/publications/fullUserStatusData.js | 30 + app/utils/client/index.js | 22 + app/utils/client/lib/CustomTranslations.js | 22 + app/utils/client/lib/RestApiClient.js | 113 + app/utils/client/lib/canDeleteMessage.js | 40 + app/utils/client/lib/handleError.js | 25 + app/utils/client/lib/roomTypes.js | 179 + app/utils/index.js | 8 + .../utils}/lib/RoomTypeConfig.js | 48 +- .../utils}/lib/RoomTypesCommon.js | 47 +- app/utils/lib/fileUploadRestrictions.js | 44 + app/utils/lib/getAvatarColor.js | 3 + app/utils/lib/getAvatarURL.js | 10 + .../utils}/lib/getDefaultSubscriptionPref.js | 2 +- app/utils/lib/getRoomAvatarURL.js | 13 + app/utils/lib/getURL.js | 28 + app/utils/lib/getUserAvatarURL.js | 19 + .../lib/getUserNotificationPreference.js | 31 + app/utils/lib/getUserPreference.js | 17 + app/utils/lib/getValidRoomName.js | 58 + app/utils/lib/isEmail.js | 23 + app/utils/lib/isURL.js | 1 + app/utils/lib/mimeTypes.js | 8 + app/utils/lib/placeholders.js | 33 + app/utils/lib/roomExit.js | 43 + app/utils/lib/slashCommand.js | 85 + app/utils/lib/tapi18n.js | 17 + app/utils/lib/templateVarHandler.js | 38 + app/utils/rocketchat.info | 3 + .../server/functions/getDefaultUserFields.js | 29 + app/utils/server/functions/getMongoInfo.js | 51 + app/utils/server/functions/isDocker.js | 31 + app/utils/server/index.js | 19 + .../server/lib/normalizeMessagesForUser.js | 56 + app/utils/server/lib/roomTypes.js | 50 + app/version-check/client/index.js | 37 + app/version-check/server/addSettings.js | 10 + .../server/functions/checkVersionUpdate.js | 102 + .../server/functions/getNewUpdates.js | 65 + app/version-check/server/index.js | 30 + app/version-check/server/logger.js | 3 + .../server/methods/banner_dismiss.js | 13 + app/version-check/server/sampleUpdateData.js | 17 + app/videobridge/.eslintrc | 5 + app/videobridge/client/actionLink.js | 22 + app/videobridge/client/index.js | 8 + .../client/stylesheets/video.less | 0 app/videobridge/client/tabBar.js | 90 + .../client/views/bbbLiveView.html | 0 .../client/views/videoFlexTab.html | 0 app/videobridge/client/views/videoFlexTab.js | 149 + .../client/views/videoFlexTabBbb.html | 0 .../client/views/videoFlexTabBbb.js | 64 + app/videobridge/constants.js | 3 + app/videobridge/lib/messageType.js | 12 + app/videobridge/server/actionLink.js | 5 + app/videobridge/server/index.js | 5 + .../videobridge}/server/methods/bbb.js | 35 +- .../server/methods/jitsiSetTimeout.js | 45 + app/videobridge/server/settings.js | 133 + app/webdav/README.md | 3 + app/webdav/client/actionButton.js | 50 + app/webdav/client/addWebdavAccount.html | 43 + app/webdav/client/addWebdavAccount.js | 77 + app/webdav/client/index.js | 10 + app/webdav/client/selectWebdavAccount.html | 13 + app/webdav/client/selectWebdavAccount.js | 44 + .../client/startup/messageBoxActions.js | 65 + app/webdav/client/startup/subscription.js | 8 + app/webdav/client/webdavFilePicker.css | 356 + app/webdav/client/webdavFilePicker.html | 131 + app/webdav/client/webdavFilePicker.js | 348 + app/webdav/server/index.js | 7 + app/webdav/server/methods/addWebdavAccount.js | 54 + .../server/methods/getFileFromWebdav.js | 35 + .../server/methods/getWebdavFileList.js | 36 + .../server/methods/removeWebdavAccount.js | 18 + .../server/methods/uploadFileToWebdav.js | 71 + .../server/publications/webdavAccounts.js | 18 + app/webdav/server/startup/settings.js | 8 + .../webrtc}/client/WebRTCClass.js | 196 +- .../webrtc}/client/adapter.js | 0 app/webrtc/client/index.js | 3 + .../webrtc}/client/screenShare.js | 7 +- app/webrtc/index.js | 8 + app/webrtc/server/index.js | 1 + app/webrtc/server/settings.js | 24 + app/wordpress/client/index.js | 1 + .../client/wordpress-login-button.css | 0 app/wordpress/lib/common.js | 101 + app/wordpress/server/index.js | 2 + app/wordpress/server/startup.js | 89 + client/head.html | 21 + client/helpers/escapeCssUrl.js | 2 + client/helpers/log.js | 2 + client/helpers/not.js | 2 + client/importPackages.js | 111 + client/importsCss.js | 41 + client/lib/handleError.js | 24 - client/main.js | 35 +- client/methods/deleteMessage.js | 42 +- client/methods/hideRoom.js | 4 + client/methods/openRoom.js | 4 + client/methods/setUserActiveStatus.js | 2 + client/methods/toggleFavorite.js | 4 + client/methods/updateMessage.js | 25 +- client/notifications/UsersNameChanged.js | 15 +- client/notifications/notification.js | 36 +- client/notifications/updateAvatar.js | 7 +- client/notifications/updateUserState.js | 7 +- client/routes/adminRouter.js | 10 + client/routes/pageNotFound.html | 10 +- client/routes/pageNotFound.js | 22 + client/routes/roomRoute.js | 21 +- client/routes/router.js | 24 +- client/routes/stylesheets/pageNotFound.css | 48 + client/startup/emailVerification.js | 9 +- client/startup/i18n.js | 23 +- client/startup/loginViaQuery.js | 3 + client/startup/roomObserve.js | 5 + client/startup/startup.js | 21 +- client/startup/unread.js | 67 +- client/startup/userSetUtcOffset.js | 6 +- client/startup/usersObserve.js | 12 +- docker-compose.yml | 37 +- imports/client/limax | 1 + imports/client/map-age-cleaner | 1 + imports/client/mem | 1 + imports/client/mimic-fn | 1 + imports/client/p-defer | 1 + imports/client/p-is-promise | 1 + imports/client/pinyin | 1 + imports/message-read-receipt/client/main.js | 6 +- .../message-read-receipt/client/message.js | 6 +- .../client/readReceipts.js | 7 +- imports/message-read-receipt/client/room.js | 17 +- .../server/api/methods/getReadReceipts.js | 3 +- .../message-read-receipt/server/dbIndexes.js | 5 - imports/message-read-receipt/server/hooks.js | 17 +- imports/message-read-receipt/server/index.js | 1 - .../server/lib/ReadReceipt.js | 46 +- .../server/models/ReadReceipts.js | 19 - .../message-read-receipt/server/settings.js | 6 +- .../client/personalAccessTokens.html | 36 +- .../client/personalAccessTokens.js | 12 +- .../server/api/methods/generateToken.js | 11 +- .../server/api/methods/regenerateToken.js | 9 +- .../server/api/methods/removeToken.js | 11 +- .../personal-access-tokens/server/index.js | 4 - .../server/models/Users.js | 39 - .../server/models/index.js | 1 - .../publications/personalAccessTokens.js | 7 +- .../personal-access-tokens/server/settings.js | 5 - imports/startup/client/index.js | 1 + imports/startup/client/listenActiveUsers.js | 108 + imports/startup/server/index.js | 1 + imports/users-presence/server/activeUsers.js | 28 + imports/users-presence/server/index.js | 1 + lib/francocatena_fix.js | 3 - mocha.opts | 2 +- package-lock.json | 24078 +++++++++------- package.json | 223 +- packages/.eslintrc | 5 + packages/autoupdate/QA.md | 115 - packages/autoupdate/README.md | 13 - packages/autoupdate/autoupdate_client.js | 176 - packages/autoupdate/autoupdate_cordova.js | 85 - packages/autoupdate/autoupdate_server.js | 198 - packages/autoupdate/package.js | 30 - packages/chatpal-search/client/route.js | 9 - .../chatpal-search/client/template/admin.js | 61 - .../client/template/result.html | 153 - .../chatpal-search/client/template/result.js | 128 - packages/chatpal-search/package.js | 40 - .../chatpal-search/server/asset/config.js | 4 - .../chatpal-search/server/provider/index.js | 449 - .../server/provider/provider.js | 346 - .../chatpal-search/server/utils/logger.js | 2 - packages/chatpal-search/server/utils/utils.js | 26 - packages/meteor-accounts-saml/package.js | 17 - packages/meteor-accounts-saml/saml_client.js | 141 - .../meteor-accounts-saml/saml_rocketchat.js | 194 - packages/meteor-accounts-saml/saml_server.js | 377 - packages/meteor-accounts-saml/saml_utils.js | 549 - .../client/autocomplete-client.js | 61 +- .../meteor-autocomplete/client/collection.js | 2 + packages/meteor-autocomplete/client/index.js | 7 + .../meteor-autocomplete/client/templates.js | 6 +- packages/meteor-autocomplete/package.js | 31 +- .../server/autocomplete-server.js | 2 + packages/meteor-autocomplete/server/index.js | 1 + packages/meteor-timesync/client/index.js | 6 + .../meteor-timesync/client/timesync-client.js | 16 +- packages/meteor-timesync/package.js | 40 +- packages/meteor-timesync/server/index.js | 1 + .../meteor-timesync/server/timesync-server.js | 7 +- .../client/accountSecurity.html | 50 - packages/rocketchat-2fa/package.js | 35 - .../rocketchat-2fa/server/loginHandler.js | 32 - .../rocketchat-2fa/server/methods/disable.js | 22 - .../rocketchat-2fa/server/methods/enable.js | 18 - .../server/methods/regenerateCodes.js | 27 - .../server/methods/validateTempToken.js | 25 - .../rocketchat-2fa/server/models/users.js | 49 - .../rocketchat-2fa/server/startup/settings.js | 19 - .../.npm/package/npm-shrinkwrap.json | 259 - packages/rocketchat-accounts/package.js | 22 - .../both/lib/actionLinks.js | 32 - .../rocketchat-action-links/client/init.js | 25 - .../client/lib/actionLinks.js | 23 - packages/rocketchat-action-links/package.js | 23 - .../server/actionLinkHandler.js | 15 - .../client/trackEvents.js | 143 - packages/rocketchat-analytics/package.js | 16 - .../rocketchat-analytics/server/settings.js | 85 - packages/rocketchat-api/package.js | 50 - packages/rocketchat-api/server/api.js | 432 - .../rocketchat-api/server/default/info.js | 15 - .../server/helpers/deprecationWarning.js | 13 - .../server/helpers/getLoggedInUser.js | 12 - .../server/helpers/getPaginationItems.js | 30 - .../server/helpers/getUserFromParams.js | 22 - .../server/helpers/getUserInfo.js | 58 - .../server/helpers/insertUserObject.js | 15 - .../server/helpers/isUserFromParams.js | 8 - .../server/helpers/parseJsonQuery.js | 80 - .../server/helpers/requestParams.js | 3 - packages/rocketchat-api/server/settings.js | 12 - packages/rocketchat-api/server/v1/assets.js | 53 - packages/rocketchat-api/server/v1/channels.js | 968 - packages/rocketchat-api/server/v1/chat.js | 338 - packages/rocketchat-api/server/v1/commands.js | 164 - packages/rocketchat-api/server/v1/e2e.js | 46 - .../rocketchat-api/server/v1/emoji-custom.js | 7 - packages/rocketchat-api/server/v1/groups.js | 785 - packages/rocketchat-api/server/v1/im.js | 353 - .../rocketchat-api/server/v1/integrations.js | 149 - packages/rocketchat-api/server/v1/misc.js | 173 - .../rocketchat-api/server/v1/permissions.js | 80 - packages/rocketchat-api/server/v1/push.js | 59 - packages/rocketchat-api/server/v1/roles.js | 51 - packages/rocketchat-api/server/v1/rooms.js | 192 - packages/rocketchat-api/server/v1/settings.js | 137 - packages/rocketchat-api/server/v1/stats.js | 42 - .../rocketchat-api/server/v1/subscriptions.js | 81 - packages/rocketchat-api/server/v1/users.js | 485 - .../assets/stylesheets/apps.css | 179 - .../client/admin/appInstall.html | 64 - .../rocketchat-apps/client/admin/appLogs.html | 70 - .../rocketchat-apps/client/admin/appLogs.js | 86 - .../client/admin/appManage.html | 446 - .../rocketchat-apps/client/admin/appManage.js | 414 - .../client/admin/appWhatIsIt.js | 39 - .../rocketchat-apps/client/admin/apps.html | 91 - packages/rocketchat-apps/client/admin/apps.js | 267 - .../client/communication/index.js | 3 - .../client/communication/websockets.js | 88 - .../rocketchat-apps/client/orchestrator.js | 151 - packages/rocketchat-apps/lib/Apps.js | 2 - .../rocketchat-apps/lib/misc/Utilities.js | 5 - packages/rocketchat-apps/package.js | 88 - .../server/bridges/commands.js | 170 - .../server/bridges/environmental.js | 32 - .../rocketchat-apps/server/bridges/http.js | 15 - .../rocketchat-apps/server/bridges/index.js | 25 - .../server/bridges/listeners.js | 39 - .../server/bridges/messages.js | 77 - .../server/bridges/persistence.js | 95 - .../rocketchat-apps/server/bridges/rooms.js | 81 - .../server/bridges/settings.js | 71 - .../rocketchat-apps/server/bridges/users.js | 17 - .../server/communication/methods.js | 89 - .../server/communication/rest.js | 345 - .../server/communication/websockets.js | 169 - .../server/converters/messages.js | 193 - .../server/converters/rooms.js | 93 - .../server/converters/settings.js | 51 - .../server/converters/users.js | 78 - .../rocketchat-apps/server/orchestrator.js | 125 - .../server/storage/apps-logs-model.js | 5 - .../server/storage/apps-model.js | 5 - .../server/storage/apps-persistence-model.js | 5 - .../rocketchat-apps/server/storage/index.js | 7 - packages/rocketchat-assets/package.js | 18 - packages/rocketchat-assets/server/assets.js | 517 - .../client/hasPermission.js | 58 - .../client/hasRole.js | 4 - .../client/lib/ChatPermissions.js | 6 - .../client/lib/models/Roles.js | 20 - .../client/lib/models/Subscriptions.js | 42 - .../client/lib/models/Users.js | 26 - .../rocketchat-authorization/client/route.js | 32 - .../client/startup.js | 12 - .../client/stylesheets/permissions.css | 67 - .../client/usersNameChanged.js | 14 - .../client/views/permissions.html | 77 - .../client/views/permissions.js | 146 - .../client/views/permissionsRole.html | 88 - .../client/views/permissionsRole.js | 238 - .../lib/rocketchat.js | 5 - packages/rocketchat-authorization/package.js | 73 - .../server/functions/addUserRoles.js | 28 - .../server/functions/canAccessRoom.js | 30 - .../server/functions/getRoles.js | 3 - .../server/functions/getUsersInRole.js | 3 - .../server/functions/hasPermission.js | 32 - .../server/functions/hasRole.js | 4 - .../server/functions/removeUserFromRoles.js | 29 - .../server/methods/addPermissionToRole.js | 28 - .../server/methods/addUserToRole.js | 53 - .../server/methods/deleteRole.js | 35 - .../methods/removeRoleFromPermission.js | 35 - .../server/methods/removeUserFromRole.js | 65 - .../server/methods/saveRole.js | 30 - .../server/models/Base.js | 47 - .../server/models/Permissions.js | 32 - .../server/models/Roles.js | 80 - .../server/models/Subscriptions.js | 35 - .../server/models/Users.js | 13 - .../server/publications/permissions.js | 43 - .../server/publications/roles.js | 7 - .../server/publications/usersInRole.js | 20 - .../server/startup.js | 188 - .../rocketchat-autolinker/client/client.js | 73 - packages/rocketchat-autolinker/package.js | 15 - .../rocketchat-autolinker/server/settings.js | 16 - .../client/lib/actionButton.js | 34 - .../client/lib/autotranslate.js | 96 - .../client/lib/tabBar.js | 16 - packages/rocketchat-autotranslate/package.js | 36 - .../server/autotranslate.js | 257 - .../server/methods/getSupportedLanguages.js | 17 - .../server/methods/saveSettings.js | 38 - .../server/methods/translateMessage.js | 8 - .../server/models/Messages.js | 17 - .../server/models/Subscriptions.js | 47 - .../server/permissions.js | 7 - .../server/settings.js | 4 - packages/rocketchat-bigbluebutton/package.js | 11 - packages/rocketchat-blockstack/client/main.js | 52 - .../rocketchat-blockstack/client/routes.js | 43 - packages/rocketchat-blockstack/package.js | 14 - .../rocketchat-blockstack/server/logger.js | 3 - .../server/loginHandler.js | 52 - .../rocketchat-blockstack/server/routes.js | 26 - .../rocketchat-blockstack/server/settings.js | 70 - packages/rocketchat-bot-helpers/package.js | 18 - .../rocketchat-bot-helpers/server/index.js | 163 - .../rocketchat-bot-helpers/server/settings.js | 10 - packages/rocketchat-cas/client/cas_client.js | 88 - packages/rocketchat-cas/package.js | 26 - .../rocketchat-cas/server/cas_rocketchat.js | 65 - packages/rocketchat-cas/server/cas_server.js | 252 - .../client/lib/startup.js | 15 - .../package.js | 30 - .../server/lib/startup.js | 9 - .../server/methods/mailMessages.js | 88 - .../client/startup/messageTypes.js | 51 - .../client/startup/tabBar.js | 11 - .../client/startup/trackSettingsChange.js | 38 - .../client/views/channelSettings.js | 828 - .../rocketchat-channel-settings/package.js | 42 - .../server/functions/saveReactWhenReadOnly.js | 7 - .../server/functions/saveRoomAnnouncement.js | 20 - .../server/functions/saveRoomCustomFields.js | 18 - .../server/functions/saveRoomDescription.js | 11 - .../server/functions/saveRoomName.js | 21 - .../server/functions/saveRoomReadOnly.js | 8 - .../functions/saveRoomSystemMessages.js | 8 - .../server/functions/saveRoomTopic.js | 13 - .../server/functions/saveRoomType.js | 41 - .../server/methods/saveRoomSettings.js | 204 - .../server/models/Messages.js | 7 - .../server/models/Rooms.js | 65 - .../server/startup.js | 5 - packages/rocketchat-colors/client/client.js | 20 - packages/rocketchat-colors/package.js | 15 - packages/rocketchat-colors/server/settings.js | 7 - packages/rocketchat-cors/common.js | 5 - packages/rocketchat-cors/cors.js | 129 - packages/rocketchat-cors/package.js | 17 - .../rocketchat-crowd/client/loginHelper.js | 23 - packages/rocketchat-crowd/package.js | 24 - packages/rocketchat-crowd/server/crowd.js | 255 - packages/rocketchat-crowd/server/settings.js | 12 - packages/rocketchat-custom-oauth/package.js | 26 - .../server/custom_oauth_server.js | 336 - .../client/admin/adminSounds.html | 49 - .../client/admin/adminSounds.js | 135 - .../client/admin/route.js | 9 - .../client/admin/startup.js | 8 - .../client/lib/CustomSounds.js | 72 - .../client/models/CustomSounds.js | 8 - .../client/notifications/deleteCustomSound.js | 5 - .../client/notifications/updateCustomSound.js | 5 - packages/rocketchat-custom-sounds/package.js | 51 - .../server/methods/deleteCustomSound.js | 22 - .../server/methods/insertOrUpdateSound.js | 64 - .../server/methods/listCustomSounds.js | 5 - .../server/methods/uploadCustomSound.js | 19 - .../server/models/CustomSounds.js | 54 - .../server/publications/customSounds.js | 27 - .../server/startup/custom-sounds.js | 78 - .../server/startup/permissions.js | 5 - .../server/startup/settings.js | 22 - packages/rocketchat-dolphin/common.js | 63 - packages/rocketchat-dolphin/package.js | 19 - packages/rocketchat-dolphin/startup.js | 8 - packages/rocketchat-drupal/common.js | 39 - packages/rocketchat-drupal/package.js | 22 - packages/rocketchat-drupal/startup.js | 14 - packages/rocketchat-e2e/.eslintrc | 4 - packages/rocketchat-e2e/client/helper.js | 117 - .../rocketchat-e2e/client/rocketchat.e2e.js | 383 - packages/rocketchat-e2e/package.js | 18 - packages/rocketchat-e2e/server/index.js | 16 - .../server/methods/addKeyToChain.js | 14 - .../server/methods/fetchKeychain.js | 11 - .../server/methods/fetchMyKeys.js | 12 - .../methods/getUsersOfRoomWithoutKey.js | 26 - .../server/methods/updateGroupE2EKey.js | 14 - .../rocketchat-e2e/server/models/Rooms.js | 15 - .../server/models/Subscriptions.js | 19 - .../rocketchat-e2e/server/models/Users.js | 56 - packages/rocketchat-e2e/server/settings.js | 9 - .../admin/adminEmoji.html | 59 - .../admin/adminEmoji.js | 121 - .../rocketchat-emoji-custom/admin/route.js | 9 - .../rocketchat-emoji-custom/admin/startup.js | 8 - .../client/lib/emojiCustom.js | 171 - .../client/models/EmojiCustom.js | 20 - .../client/notifications/deleteEmojiCustom.js | 4 - .../client/notifications/updateEmojiCustom.js | 4 - .../rocketchat-emoji-custom/function-isSet.js | 25 - packages/rocketchat-emoji-custom/package.js | 57 - .../server/methods/deleteEmojiCustom.js | 22 - .../server/methods/insertOrUpdateEmoji.js | 106 - .../server/methods/listEmojiCustom.js | 5 - .../server/methods/uploadEmojiCustom.js | 21 - .../server/models/EmojiCustom.js | 89 - .../server/publications/fullEmojiData.js | 28 - .../server/startup/emoji-custom.js | 108 - .../server/startup/settings.js | 22 - packages/rocketchat-emoji-emojione/README.md | 22 - .../client/sprites.css | 7356 ----- .../rocketchat-emoji-emojione/emojiPicker.js | 1529 - .../emojione.sprites.mustache | 34 - .../generateEmojiIndex.js | 31 - packages/rocketchat-emoji-emojione/package.js | 22 - .../rocketchat-emoji-emojione/rocketchat.js | 45 - .../server/callbacks.js | 4 - .../rocketchat-emoji/client/emojiButton.js | 37 - .../rocketchat-emoji/client/emojiParser.js | 51 - .../rocketchat-emoji/client/emojiPicker.html | 43 - .../rocketchat-emoji/client/emojiPicker.js | 283 - .../rocketchat-emoji/client/function-isSet.js | 25 - .../rocketchat-emoji/client/keyboardFix.js | 14 - .../client/lib/EmojiPicker.js | 141 - .../client/lib/emojiRenderer.js | 23 - .../rocketchat-emoji/client/rocketchat.js | 15 - packages/rocketchat-emoji/package.js | 31 - packages/rocketchat-error-handler/package.js | 17 - .../server/lib/RocketChat.ErrorHandler.js | 69 - .../server/startup/settings.js | 3 - packages/rocketchat-favico/package.js | 9 - .../client/lib/fileUploadHandler.js | 35 - .../globalFileRestrictions.js | 29 - .../rocketchat-file-upload/lib/FileUpload.js | 55 - .../lib/FileUploadBase.js | 78 - packages/rocketchat-file-upload/package.js | 46 - .../server/config/AmazonS3.js | 106 - .../server/config/Slingshot_DEPRECATED.js | 114 - .../server/config/Webdav.js | 62 - .../server/config/_configUploadStorage.js | 22 - .../server/lib/FileUpload.js | 377 - .../server/lib/proxy.js | 91 - .../server/lib/requests.js | 23 - .../server/methods/getS3FileUrl.js | 18 - .../server/methods/sendFileMessage.js | 74 - .../server/startup/settings.js | 237 - .../ufs/AmazonS3/client.js | 6 - .../ufs/AmazonS3/server.js | 157 - .../ufs/GoogleStorage/client.js | 6 - .../ufs/GoogleStorage/server.js | 123 - .../ufs/Webdav/client.js | 6 - .../ufs/Webdav/server.js | 135 - packages/rocketchat-file/package.js | 16 - .../rocketchat-github-enterprise/common.js | 35 - .../rocketchat-github-enterprise/package.js | 17 - .../rocketchat-github-enterprise/startup.js | 14 - packages/rocketchat-gitlab/common.js | 30 - packages/rocketchat-gitlab/package.js | 17 - packages/rocketchat-gitlab/startup.js | 14 - .../client/index.js | 10 +- .../package.js | 15 +- .../server/index.js | 15 +- .../server/models/Rooms.js | 3 - .../server/settings.js | 7 +- .../client/googlevision.js | 84 - packages/rocketchat-google-vision/package.js | 16 - .../server/googlevision.js | 153 - .../server/models/Messages.js | 10 - .../server/settings.js | 88 - packages/rocketchat-grant-facebook/package.js | 16 - .../rocketchat-grant-facebook/server/index.js | 56 - packages/rocketchat-grant-github/package.js | 16 - .../rocketchat-grant-github/server/index.js | 46 - packages/rocketchat-grant-google/package.js | 16 - .../rocketchat-grant-google/server/index.js | 38 - .../rocketchat-grant/.npm/package/.gitignore | 1 - packages/rocketchat-grant/.npm/package/README | 7 - .../.npm/package/npm-shrinkwrap.json | 539 - packages/rocketchat-grant/package.js | 25 - packages/rocketchat-grant/server/error.js | 5 - packages/rocketchat-grant/server/index.js | 57 - packages/rocketchat-grant/server/providers.js | 41 - packages/rocketchat-grant/server/settings.js | 42 - .../.npm/package/.gitignore | 1 - .../rocketchat-graphql/.npm/package/README | 7 - .../.npm/package/npm-shrinkwrap.json | 714 - packages/rocketchat-graphql/package.js | 34 - packages/rocketchat-graphql/server/api.js | 75 - .../server/resolvers/accounts/index.js | 21 - .../server/resolvers/channels/Channel-type.js | 51 - .../server/resolvers/channels/channels.js | 56 - .../resolvers/channels/channelsByUser.js | 32 - .../resolvers/channels/createChannel.js | 40 - .../server/resolvers/channels/index.js | 49 - .../resolvers/messages/deleteMessage.js | 32 - .../server/resolvers/messages/messages.js | 91 - .../server/resolvers/messages/sendMessage.js | 28 - .../server/resolvers/users/User-type.js | 28 - .../server/resolvers/users/setStatus.js | 23 - .../rocketchat-graphql/server/settings.js | 7 - .../client/client.js | 31 - .../rocketchat-highlight-words/package.js | 20 - packages/rocketchat-i18n/.eslintrc | 5 + packages/rocketchat-i18n/i18n/af.i18n.json | 122 +- packages/rocketchat-i18n/i18n/ar.i18n.json | 62 +- packages/rocketchat-i18n/i18n/az.i18n.json | 118 +- .../rocketchat-i18n/i18n/bas-CM.i18n.json | 1 + packages/rocketchat-i18n/i18n/be-BY.i18n.json | 104 +- packages/rocketchat-i18n/i18n/bg.i18n.json | 124 +- packages/rocketchat-i18n/i18n/bs.i18n.json | 2841 +- packages/rocketchat-i18n/i18n/ca.i18n.json | 197 +- packages/rocketchat-i18n/i18n/cs.i18n.json | 100 +- packages/rocketchat-i18n/i18n/cy.i18n.json | 253 +- packages/rocketchat-i18n/i18n/da.i18n.json | 254 +- packages/rocketchat-i18n/i18n/de-AT.i18n.json | 123 +- packages/rocketchat-i18n/i18n/de-IN.i18n.json | 1638 +- packages/rocketchat-i18n/i18n/de.i18n.json | 1698 +- packages/rocketchat-i18n/i18n/el.i18n.json | 100 +- packages/rocketchat-i18n/i18n/en.i18n.json | 495 +- packages/rocketchat-i18n/i18n/eo.i18n.json | 122 +- packages/rocketchat-i18n/i18n/es.i18n.json | 211 +- packages/rocketchat-i18n/i18n/et.i18n.json | 75 + packages/rocketchat-i18n/i18n/eu.i18n.json | 96 + packages/rocketchat-i18n/i18n/fa.i18n.json | 207 +- packages/rocketchat-i18n/i18n/fi.i18n.json | 88 +- packages/rocketchat-i18n/i18n/fr.i18n.json | 120 +- packages/rocketchat-i18n/i18n/he.i18n.json | 48 +- packages/rocketchat-i18n/i18n/hi-IN.i18n.json | 198 + packages/rocketchat-i18n/i18n/hr.i18n.json | 234 +- packages/rocketchat-i18n/i18n/hu.i18n.json | 1131 +- packages/rocketchat-i18n/i18n/id.i18n.json | 99 +- packages/rocketchat-i18n/i18n/it.i18n.json | 115 +- packages/rocketchat-i18n/i18n/ja.i18n.json | 1732 +- packages/rocketchat-i18n/i18n/km.i18n.json | 380 +- packages/rocketchat-i18n/i18n/ko.i18n.json | 3048 +- packages/rocketchat-i18n/i18n/ku.i18n.json | 121 +- packages/rocketchat-i18n/i18n/lo.i18n.json | 107 +- packages/rocketchat-i18n/i18n/lt.i18n.json | 108 +- packages/rocketchat-i18n/i18n/lv.i18n.json | 91 +- packages/rocketchat-i18n/i18n/mn.i18n.json | 78 +- packages/rocketchat-i18n/i18n/ms-MY.i18n.json | 121 +- packages/rocketchat-i18n/i18n/nl.i18n.json | 418 +- packages/rocketchat-i18n/i18n/no.i18n.json | 127 +- packages/rocketchat-i18n/i18n/pl.i18n.json | 693 +- packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 642 +- packages/rocketchat-i18n/i18n/pt.i18n.json | 522 +- packages/rocketchat-i18n/i18n/ro.i18n.json | 102 +- packages/rocketchat-i18n/i18n/ru.i18n.json | 575 +- packages/rocketchat-i18n/i18n/sk-SK.i18n.json | 123 +- packages/rocketchat-i18n/i18n/sl-SI.i18n.json | 86 +- packages/rocketchat-i18n/i18n/sq.i18n.json | 130 +- packages/rocketchat-i18n/i18n/sr.i18n.json | 184 +- packages/rocketchat-i18n/i18n/sv.i18n.json | 331 +- packages/rocketchat-i18n/i18n/ta-IN.i18n.json | 114 +- packages/rocketchat-i18n/i18n/th-TH.i18n.json | 85 +- packages/rocketchat-i18n/i18n/tr.i18n.json | 1948 +- packages/rocketchat-i18n/i18n/ug.i18n.json | 29 +- packages/rocketchat-i18n/i18n/uk.i18n.json | 144 +- packages/rocketchat-i18n/i18n/vi-VN.i18n.json | 121 +- packages/rocketchat-i18n/i18n/zh-HK.i18n.json | 77 +- packages/rocketchat-i18n/i18n/zh-TW.i18n.json | 127 +- packages/rocketchat-i18n/i18n/zh.i18n.json | 1622 +- packages/rocketchat-i18n/package.js | 30 +- .../rocketchat-iframe-login/iframe_client.js | 322 - .../rocketchat-iframe-login/iframe_server.js | 26 - packages/rocketchat-iframe-login/package.js | 33 - .../rocketchat-importer-csv/client/adder.js | 4 - packages/rocketchat-importer-csv/info.js | 10 - packages/rocketchat-importer-csv/package.js | 25 - .../rocketchat-importer-csv/server/adder.js | 5 - .../server/importer.js | 369 - .../client/adder.js | 4 - .../info.js | 12 - .../package.js | 25 - .../server/adder.js | 5 - .../server/importer.js | 544 - .../client/adder.js | 4 - packages/rocketchat-importer-hipchat/info.js | 7 - .../rocketchat-importer-hipchat/package.js | 25 - .../server/adder.js | 5 - .../server/importer.js | 358 - .../client/adder.js | 4 - .../rocketchat-importer-slack-users/info.js | 10 - .../package.js | 25 - .../server/adder.js | 5 - .../server/importer.js | 142 - .../rocketchat-importer-slack/client/adder.js | 4 - packages/rocketchat-importer-slack/info.js | 7 - packages/rocketchat-importer-slack/package.js | 25 - .../rocketchat-importer-slack/server/adder.js | 5 - .../server/importer.js | 497 - .../client/admin/adminImport.html | 24 - .../client/admin/adminImport.js | 29 - .../client/admin/adminImportPrepare.html | 87 - .../client/admin/adminImportPrepare.js | 197 - .../client/admin/adminImportProgress.html | 5 - packages/rocketchat-importer/client/index.js | 11 - packages/rocketchat-importer/package.js | 56 - .../server/classes/ImporterBase.js | 333 - packages/rocketchat-importer/server/index.js | 25 - .../server/methods/restartImport.js | 31 - .../server/models/Imports.js | 7 - .../server/models/RawImports.js | 7 - .../client/collections.js | 2 - .../rocketchat-integrations/client/route.js | 71 - .../rocketchat-integrations/client/startup.js | 6 - .../client/views/integrations.js | 17 - .../client/views/integrationsNew.js | 5 - .../rocketchat-integrations/lib/rocketchat.js | 67 - packages/rocketchat-integrations/package.js | 72 - .../rocketchat-integrations/server/api/api.js | 403 - .../rocketchat-integrations/server/logger.js | 9 - .../server/methods/clearIntegrationHistory.js | 21 - .../incoming/addIncomingIntegration.js | 99 - .../incoming/deleteIncomingIntegration.js | 21 - .../incoming/updateIncomingIntegration.js | 107 - .../outgoing/addOutgoingIntegration.js | 18 - .../outgoing/deleteOutgoingIntegration.js | 22 - .../outgoing/replayOutgoingIntegration.js | 27 - .../outgoing/updateOutgoingIntegration.js | 55 - .../server/models/Integrations.js | 17 - .../server/processWebhookMessage.js | 84 - .../server/publications/integrationHistory.js | 13 - .../server/publications/integrations.js | 13 - .../server/triggers.js | 14 - .../.npm/package/.gitignore | 1 - .../.npm/package/README | 7 - .../.npm/package/npm-shrinkwrap.json | 715 - packages/rocketchat-internal-hubot/README.md | 21 - packages/rocketchat-internal-hubot/hubot.js | 259 - packages/rocketchat-internal-hubot/package.js | 34 - .../rocketchat-internal-hubot/settings.js | 13 - .../rocketchat-irc/.npm/package/.gitignore | 1 - packages/rocketchat-irc/.npm/package/README | 7 - .../.npm/package/npm-shrinkwrap.json | 20 - packages/rocketchat-irc/package.js | 22 - .../rocketchat-irc/server/irc-bridge/index.js | 138 - .../irc-bridge/localHandlers/onCreateRoom.js | 15 - .../irc-bridge/peerHandlers/disconnected.js | 13 - .../irc-bridge/peerHandlers/joinedChannel.js | 22 - .../irc-bridge/peerHandlers/leftChannel.js | 18 - .../irc-bridge/peerHandlers/sentMessage.js | 82 - packages/rocketchat-irc/server/irc.js | 24 - .../server/methods/resetIrcConnection.js | 52 - .../server/servers/RFC2813/index.js | 183 - packages/rocketchat-issuelinks/client.js | 18 - packages/rocketchat-issuelinks/package.js | 14 - packages/rocketchat-issuelinks/settings.js | 17 - packages/rocketchat-katex/katex.js | 248 - packages/rocketchat-katex/package-lock.json | 21 - packages/rocketchat-katex/package.js | 29 - packages/rocketchat-katex/package.json | 18 - packages/rocketchat-katex/settings.js | 32 - packages/rocketchat-lazy-load/client/index.js | 65 - .../client/lazyloadImage.js | 34 - packages/rocketchat-lazy-load/package.js | 16 - .../rocketchat-ldap/client/loginHelper.js | 39 - packages/rocketchat-ldap/package.js | 24 - .../rocketchat-ldap/server/loginHandler.js | 157 - packages/rocketchat-ldap/server/settings.js | 94 - packages/rocketchat-ldap/server/syncUsers.js | 27 - packages/rocketchat-lib/README.md | 159 - .../client/CustomTranslations.js | 26 - .../rocketchat-lib/client/MessageAction.js | 352 - .../rocketchat-lib/client/Notifications.js | 84 - packages/rocketchat-lib/client/OAuthProxy.js | 14 - packages/rocketchat-lib/client/UserDeleted.js | 7 - .../rocketchat-lib/client/defaultTabBars.js | 66 - .../client/lib/ChannelActions.js | 103 - packages/rocketchat-lib/client/lib/Layout.js | 11 - .../client/lib/RestApiClient.js | 111 - packages/rocketchat-lib/client/lib/TabBar.js | 82 - .../client/lib/cachedCollection.js | 392 - .../rocketchat-lib/client/lib/formatDate.js | 24 - packages/rocketchat-lib/client/lib/index.js | 30 - .../rocketchat-lib/client/lib/openRoom.js | 95 - .../rocketchat-lib/client/lib/roomExit.js | 39 - .../rocketchat-lib/client/lib/roomTypes.js | 133 - .../rocketchat-lib/client/lib/settings.js | 92 - .../client/lib/startup/commands.js | 17 - .../client/methods/sendMessage.js | 27 - .../rocketchat-lib/client/models/Avatars.js | 6 - .../rocketchat-lib/client/models/Uploads.js | 7 - .../client/models/UserDataFiles.js | 6 - .../rocketchat-lib/client/models/_Base.js | 52 - packages/rocketchat-lib/lib/Message.js | 31 - .../rocketchat-lib/lib/MessageProperties.js | 20 - packages/rocketchat-lib/lib/MessageTypes.js | 167 - packages/rocketchat-lib/lib/callbacks.js | 149 - packages/rocketchat-lib/lib/core.js | 7 - .../lib/fileUploadRestrictions.js | 36 - packages/rocketchat-lib/lib/getAvatarColor.js | 5 - packages/rocketchat-lib/lib/getURL.js | 20 - .../lib/getUserNotificationPreference.js | 28 - .../rocketchat-lib/lib/getUserPreference.js | 18 - .../rocketchat-lib/lib/getValidRoomName.js | 48 - packages/rocketchat-lib/lib/messageBox.js | 58 - packages/rocketchat-lib/lib/placeholders.js | 32 - packages/rocketchat-lib/lib/promises.js | 82 - .../lib/roomTypes/conversation.js | 16 - .../rocketchat-lib/lib/roomTypes/direct.js | 139 - .../rocketchat-lib/lib/roomTypes/favorite.js | 16 - .../rocketchat-lib/lib/roomTypes/private.js | 90 - .../rocketchat-lib/lib/roomTypes/public.js | 92 - .../rocketchat-lib/lib/roomTypes/unread.js | 17 - packages/rocketchat-lib/lib/settings.js | 100 - packages/rocketchat-lib/lib/slashCommand.js | 83 - .../lib/startup/settingsOnLoadSiteUrl.js | 26 - .../rocketchat-lib/lib/templateVarHandler.js | 37 - packages/rocketchat-lib/package.js | 253 - packages/rocketchat-lib/rocketchat.info | 4 - .../server/functions/Notifications.js | 145 - .../functions/addUserToDefaultChannels.js | 30 - .../server/functions/addUserToRoom.js | 50 - .../server/functions/archiveRoom.js | 6 - .../functions/checkEmailAvailability.js | 5 - .../functions/checkUsernameAvailability.js | 26 - .../server/functions/cleanRoomHistory.js | 39 - .../server/functions/createRoom.js | 116 - .../server/functions/deleteMessage.js | 55 - .../server/functions/deleteUser.js | 91 - .../server/functions/getFullUserData.js | 82 - .../getRoomByNameOrIdWithOptionToJoin.js | 80 - .../server/functions/isDocker.js | 31 - .../server/functions/loadMessageHistory.js | 88 - .../server/functions/notifications/audio.js | 37 - .../server/functions/notifications/desktop.js | 64 - .../server/functions/notifications/email.js | 183 - .../server/functions/notifications/index.js | 70 - .../server/functions/notifications/mobile.js | 90 - .../server/functions/removeUserFromRoom.js | 25 - .../server/functions/saveCustomFields.js | 8 - .../saveCustomFieldsWithoutValidation.js | 37 - .../server/functions/saveUser.js | 259 - .../server/functions/sendMessage.js | 185 - .../server/functions/setEmail.js | 38 - .../server/functions/setRealName.js | 35 - .../server/functions/setUsername.js | 86 - .../server/functions/settings.js | 292 - .../server/functions/unarchiveRoom.js | 4 - .../server/functions/updateMessage.js | 50 - .../server/lib/PushNotification.js | 54 - .../rocketchat-lib/server/lib/RateLimiter.js | 43 - packages/rocketchat-lib/server/lib/bugsnag.js | 33 - .../rocketchat-lib/server/lib/configLogger.js | 15 - packages/rocketchat-lib/server/lib/debug.js | 94 - packages/rocketchat-lib/server/lib/index.js | 17 - packages/rocketchat-lib/server/lib/metrics.js | 183 - .../server/lib/notifyUsersOnMessage.js | 115 - .../server/lib/passwordPolicy.js | 13 - .../server/lib/processDirectEmail.js | 118 - .../rocketchat-lib/server/lib/roomTypes.js | 49 - .../server/lib/sendNotificationsOnMessage.js | 270 - .../server/lib/validateEmailDomain.js | 44 - .../server/methods/addOAuthService.js | 36 - .../server/methods/addUserToRoom.js | 8 - .../server/methods/addUsersToRoom.js | 67 - .../server/methods/archiveRoom.js | 26 - .../server/methods/blockUser.js | 22 - .../methods/checkRegistrationSecretURL.js | 8 - .../methods/checkUsernameAvailability.js | 24 - .../server/methods/cleanRoomHistory.js | 26 - .../server/methods/createChannel.js | 15 - .../server/methods/createPrivateGroup.js | 27 - .../server/methods/createToken.js | 13 - .../server/methods/deleteMessage.js | 55 - .../server/methods/deleteUserOwnAccount.js | 38 - .../methods/executeSlashCommandPreview.js | 30 - .../server/methods/filterATAllTag.js | 34 - .../server/methods/filterATHereTag.js | 34 - .../server/methods/filterBadWords.js | 21 - .../server/methods/getChannelHistory.js | 95 - .../server/methods/getFullUserData.js | 6 - .../server/methods/getRoomJoinCode.js | 17 - .../server/methods/getRoomRoles.js | 39 - .../server/methods/getServerInfo.js | 5 - .../server/methods/getSingleMessage.js | 19 - .../server/methods/getSlashCommandPreviews.js | 24 - .../server/methods/getUserRoles.js | 31 - .../server/methods/insertOrUpdateUser.js | 12 - .../server/methods/joinDefaultChannels.js | 12 - .../rocketchat-lib/server/methods/joinRoom.js | 35 - .../server/methods/leaveRoom.js | 34 - .../server/methods/refreshOAuthService.js | 15 - .../server/methods/removeOAuthService.js | 35 - .../server/methods/robotMethods.js | 27 - .../server/methods/saveSetting.js | 44 - .../server/methods/sendInvitationEmail.js | 62 - .../server/methods/sendMessage.js | 92 - .../server/methods/sendSMTPTestEmail.js | 44 - .../server/methods/setAdminStatus.js | 23 - .../rocketchat-lib/server/methods/setEmail.js | 30 - .../server/methods/setRealName.js | 24 - .../server/methods/setUsername.js | 54 - .../server/methods/unarchiveRoom.js | 22 - .../server/methods/unblockUser.js | 22 - .../server/methods/updateMessage.js | 53 - .../rocketchat-lib/server/models/Avatars.js | 113 - .../rocketchat-lib/server/models/Messages.js | 831 - .../rocketchat-lib/server/models/Rooms.js | 801 - .../rocketchat-lib/server/models/Settings.js | 182 - .../server/models/Subscriptions.js | 848 - .../rocketchat-lib/server/models/Uploads.js | 110 - .../server/models/UserDataFiles.js | 40 - .../rocketchat-lib/server/models/Users.js | 630 - .../rocketchat-lib/server/models/_Base.js | 280 - .../rocketchat-lib/server/models/_BaseDb.js | 380 - .../rocketchat-lib/server/oauth/facebook.js | 63 - packages/rocketchat-lib/server/oauth/oauth.js | 53 - packages/rocketchat-lib/server/oauth/proxy.js | 12 - .../server/publications/settings.js | 93 - .../server/startup/oAuthServicesUpdate.js | 96 - .../rocketchat-lib/server/startup/settings.js | 2940 -- .../server/startup/settingsOnLoadCdnPrefix.js | 32 - .../startup/settingsOnLoadDirectReply.js | 66 - .../server/startup/settingsOnLoadSMTP.js | 65 - .../startup/defaultRoomTypes.js | 15 - packages/rocketchat-lib/tests/server.tests.js | 224 - packages/rocketchat-livechat/.app/.eslintrc | 31 + .../rocketchat-livechat/.app/.meteor/packages | 32 +- .../rocketchat-livechat/.app/.meteor/release | 2 +- .../rocketchat-livechat/.app/.meteor/versions | 89 +- .../.app/client/lib/CustomFields.js | 3 + .../.app/client/lib/LivechatFileUpload.js | 7 +- .../.app/client/lib/LivechatVideoCall.js | 23 +- .../.app/client/lib/_livechat.js | 73 +- .../.app/client/lib/chatMessages.js | 27 +- .../.app/client/lib/collections.js | 2 + .../.app/client/lib/commands.js | 9 +- .../.app/client/lib/error.js | 4 + .../.app/client/lib/fromApp/Notifications.js | 18 +- .../client/lib/fromApp/RoomHistoryManager.js | 114 +- .../.app/client/lib/fromApp/avatar.js | 5 +- .../.app/client/lib/hooks.js | 4 + .../.app/client/lib/msgTyping.js | 13 +- .../.app/client/lib/tapi18n.js | 19 +- .../.app/client/lib/triggers.js | 14 +- .../client/methods/sendMessageExternal.js | 5 +- .../.app/client/routes/router.js | 3 + .../.app/client/startup/customFields.js | 1 + .../.app/client/startup/visitor.js | 4 + .../.app/client/stylesheets/main.less | 24 +- .../.app/client/views/avatar.js | 4 + .../.app/client/views/livechatWindow.js | 60 +- .../.app/client/views/message.js | 7 +- .../.app/client/views/messageAttachment.js | 2 + .../.app/client/views/messages.html | 34 +- .../.app/client/views/messages.js | 23 +- .../.app/client/views/offlineForm.js | 14 +- .../.app/client/views/options.js | 3 + .../.app/client/views/register.js | 58 +- .../.app/client/views/survey.js | 3 + .../.app/client/views/switchDepartment.js | 4 + .../.app/client/views/videoCall.js | 2 + .../.app/i18n/af.i18n.json | 1 + .../.app/i18n/ar.i18n.json | 1 + .../.app/i18n/az.i18n.json | 1 + .../.app/i18n/bas-CM.i18n.json | 8 + .../.app/i18n/be-BY.i18n.json | 2 + .../.app/i18n/bg.i18n.json | 1 + .../.app/i18n/bs.i18n.json | 27 +- .../.app/i18n/ca.i18n.json | 5 + .../.app/i18n/cs.i18n.json | 5 + .../.app/i18n/cy.i18n.json | 1 + .../.app/i18n/da.i18n.json | 3 + .../.app/i18n/de-AT.i18n.json | 7 +- .../.app/i18n/de.i18n.json | 11 +- .../.app/i18n/el.i18n.json | 1 + .../.app/i18n/en.i18n.json | 4 +- .../.app/i18n/eo.i18n.json | 1 + .../.app/i18n/es.i18n.json | 5 + .../.app/i18n/et.i18n.json | 44 + .../.app/i18n/eu.i18n.json | 36 + .../.app/i18n/fa.i18n.json | 31 +- .../.app/i18n/fi.i18n.json | 1 + .../.app/i18n/fr.i18n.json | 2 + .../.app/i18n/he.i18n.json | 29 +- .../.app/i18n/hi-IN.i18n.json | 51 + .../.app/i18n/hr.i18n.json | 13 +- .../.app/i18n/hu.i18n.json | 23 +- .../.app/i18n/id.i18n.json | 1 + .../.app/i18n/it.i18n.json | 4 + .../.app/i18n/ja.i18n.json | 25 +- .../.app/i18n/km.i18n.json | 16 +- .../.app/i18n/ko.i18n.json | 26 +- .../.app/i18n/ku.i18n.json | 1 + .../.app/i18n/lo.i18n.json | 1 + .../.app/i18n/lt.i18n.json | 1 + .../.app/i18n/lv.i18n.json | 1 + .../.app/i18n/mn.i18n.json | 1 + .../.app/i18n/ms-MY.i18n.json | 1 + .../.app/i18n/nl.i18n.json | 37 +- .../.app/i18n/no.i18n.json | 4 +- .../.app/i18n/pl.i18n.json | 26 +- .../.app/i18n/pt-BR.i18n.json | 6 +- .../.app/i18n/pt.i18n.json | 6 +- .../.app/i18n/ro.i18n.json | 1 + .../.app/i18n/ru.i18n.json | 4 + .../.app/i18n/sk-SK.i18n.json | 1 + .../.app/i18n/sl-SI.i18n.json | 1 + .../.app/i18n/sq.i18n.json | 1 + .../.app/i18n/sr.i18n.json | 1 + .../.app/i18n/sv.i18n.json | 4 + .../.app/i18n/ta-IN.i18n.json | 1 + .../.app/i18n/th-TH.i18n.json | 1 + .../.app/i18n/tr.i18n.json | 17 +- .../.app/i18n/ug.i18n.json | 1 + .../.app/i18n/uk.i18n.json | 1 + .../.app/i18n/vi-VN.i18n.json | 1 + .../.app/i18n/zh-HK.i18n.json | 2 + .../.app/i18n/zh-TW.i18n.json | 4 + .../.app/i18n/zh.i18n.json | 9 +- .../.app/imports/client/visitor.js | 11 +- .../.app/package-lock.json | 812 +- .../rocketchat-livechat/.app/package.json | 18 +- .../assets/rocket-livechat.js | 31 +- .../client/collections/AgentUsers.js | 1 - .../client/collections/LivechatCustomField.js | 1 - .../client/collections/LivechatDepartment.js | 1 - .../collections/LivechatDepartmentAgents.js | 1 - .../client/collections/LivechatInquiry.js | 1 - .../client/collections/LivechatIntegration.js | 1 - .../client/collections/LivechatMonitoring.js | 1 - .../client/collections/LivechatPageVisited.js | 1 - .../client/collections/LivechatQueueUser.js | 1 - .../client/collections/LivechatTrigger.js | 1 - .../client/collections/LivechatVisitor.js | 1 - .../client/collections/livechatOfficeHour.js | 1 - .../client/methods/changeLivechatStatus.js | 13 - .../rocketchat-livechat/client/roomType.js | 3 - packages/rocketchat-livechat/client/route.js | 163 - .../client/startup/notifyUnreadRooms.js | 44 - packages/rocketchat-livechat/client/ui.js | 51 - .../livechatIntegrationWebhook.html | 55 - .../client/views/app/livechatAppearance.html | 207 - .../client/views/app/livechatAppearance.js | 393 - .../views/app/livechatCurrentChats.html | 74 - .../client/views/app/livechatCurrentChats.js | 109 - .../views/app/livechatCustomFields.html | 29 - .../client/views/app/livechatCustomFields.js | 45 - .../views/app/livechatDepartmentForm.html | 91 - .../client/views/app/livechatDepartments.html | 32 - .../client/views/app/livechatDepartments.js | 45 - .../views/app/livechatInstallation.html | 10 - .../client/views/app/livechatInstallation.js | 18 - .../views/app/livechatIntegrations.html | 55 - .../client/views/app/livechatOfficeHours.html | 61 - .../client/views/app/livechatOfficeHours.js | 158 - .../client/views/app/livechatQueue.js | 62 - .../client/views/app/livechatTriggers.html | 28 - .../client/views/app/livechatTriggers.js | 74 - .../client/views/app/livechatUsers.html | 72 - .../client/views/app/livechatUsers.js | 169 - .../client/views/app/tabbar/externalSearch.js | 22 - .../client/views/app/tabbar/visitorForward.js | 81 - .../client/views/app/tabbar/visitorHistory.js | 43 - .../client/views/app/tabbar/visitorInfo.js | 251 - .../client/views/sideNav/livechat.js | 135 - .../client/views/sideNav/livechatFlex.js | 19 - packages/rocketchat-livechat/config.js | 393 - .../imports/LivechatRoomType.js | 84 - .../imports/server/rest/departments.js | 103 - .../imports/server/rest/facebook.js | 91 - .../imports/server/rest/upload.js | 97 - .../imports/server/rest/users.js | 141 - packages/rocketchat-livechat/livechat.js | 60 - packages/rocketchat-livechat/messageTypes.js | 45 - packages/rocketchat-livechat/package.js | 254 +- packages/rocketchat-livechat/permissions.js | 23 - .../plugin/build-livechat.js | 26 +- packages/rocketchat-livechat/plugin/build.sh | 29 +- packages/rocketchat-livechat/server/api.js | 5 - .../server/api/lib/livechat.js | 99 - .../server/api/v1/agent.js | 72 - .../server/api/v1/config.js | 34 - .../server/api/v1/customField.js | 62 - .../server/api/v1/message.js | 256 - .../server/api/v1/offlineMessage.js | 20 - .../server/api/v1/pageVisited.js | 42 - .../rocketchat-livechat/server/api/v1/room.js | 153 - .../server/api/v1/transcript.js | 20 - .../server/api/v1/videoCall.js | 45 - .../server/api/v1/visitor.js | 88 - .../server/hooks/RDStation.js | 53 - .../server/hooks/externalMessage.js | 64 - .../server/hooks/leadCapture.js | 45 - .../server/hooks/markRoomResponded.js | 27 - .../server/hooks/offlineMessage.js | 17 - .../server/hooks/saveAnalyticsData.js | 62 - .../server/hooks/sendToCRM.js | 104 - .../server/hooks/sendToFacebook.js | 36 - .../server/lib/Livechat.js | 891 - .../server/lib/OfficeClock.js | 10 - .../server/lib/OmniChannel.js | 66 - .../server/lib/QueueMethods.js | 179 - .../server/methods/addAgent.js | 9 - .../server/methods/addManager.js | 9 - .../server/methods/changeLivechatStatus.js | 13 - .../server/methods/closeByVisitor.js | 21 - .../server/methods/closeRoom.js | 27 - .../server/methods/facebook.js | 62 - .../server/methods/getAgentData.js | 21 - .../server/methods/getAgentOverviewData.js | 16 - .../server/methods/getAnalyticsChartData.js | 16 - .../methods/getAnalyticsOverviewData.js | 16 - .../server/methods/getCustomFields.js | 5 - .../server/methods/getInitialData.js | 95 - .../server/methods/getNextAgent.js | 25 - .../server/methods/loadHistory.js | 13 - .../server/methods/loginByToken.js | 15 - .../server/methods/pageVisited.js | 5 - .../server/methods/registerGuest.js | 49 - .../server/methods/removeAgent.js | 9 - .../server/methods/removeCustomField.js | 17 - .../server/methods/removeDepartment.js | 9 - .../server/methods/removeManager.js | 9 - .../server/methods/removeRoom.js | 31 - .../server/methods/removeTrigger.js | 11 - .../server/methods/returnAsInquiry.js | 9 - .../server/methods/saveAppearance.js | 36 - .../server/methods/saveCustomField.js | 28 - .../server/methods/saveDepartment.js | 9 - .../server/methods/saveInfo.js | 40 - .../server/methods/saveIntegration.js | 35 - .../server/methods/saveOfficeHours.js | 5 - .../server/methods/saveTrigger.js | 23 - .../server/methods/searchAgent.js | 21 - .../server/methods/sendMessageLivechat.js | 40 - .../server/methods/sendOfflineMessage.js | 21 - .../server/methods/sendTranscript.js | 18 - .../server/methods/setCustomField.js | 17 - .../server/methods/setDepartmentForVisitor.js | 26 - .../server/methods/startFileUploadRoom.js | 17 - .../server/methods/startVideoCall.js | 34 - .../server/methods/takeInquiry.js | 70 - .../server/methods/transfer.js | 28 - .../server/models/LivechatCustomField.js | 45 - .../server/models/LivechatDepartment.js | 83 - .../server/models/LivechatDepartmentAgents.js | 135 - .../server/models/LivechatExternalMessage.js | 18 - .../server/models/LivechatInquiry.js | 93 - .../server/models/LivechatPageVisited.js | 47 - .../server/models/LivechatTrigger.js | 30 - .../server/models/Messages.js | 27 - .../server/models/Rooms.js | 348 - .../server/models/Users.js | 180 - .../server/models/indexes.js | 5 - .../server/publications/customFields.js | 18 - .../server/publications/departmentAgents.js | 11 - .../server/publications/externalMessages.js | 3 - .../server/publications/livechatAgents.js | 29 - .../server/publications/livechatAppearance.js | 50 - .../publications/livechatDepartments.js | 16 - .../server/publications/livechatInquiries.js | 16 - .../publications/livechatIntegration.js | 29 - .../server/publications/livechatManagers.js | 29 - .../server/publications/livechatMonitoring.js | 37 - .../publications/livechatOfficeHours.js | 7 - .../server/publications/livechatQueue.js | 47 - .../server/publications/livechatTriggers.js | 15 - .../server/publications/visitorHistory.js | 40 - .../server/publications/visitorInfo.js | 19 - .../server/publications/visitorPageVisited.js | 35 - .../rocketchat-livechat/server/roomType.js | 29 - .../server/sendMessageBySMS.js | 44 - .../rocketchat-livechat/server/startup.js | 23 - .../server/unclosedLivechats.js | 90 - .../server/visitorStatus.js | 9 - .../rocketchat-livestream/client/oauth.js | 13 - .../rocketchat-livestream/client/tabBar.js | 18 - packages/rocketchat-livestream/package.js | 37 - .../server/functions/saveStreamingOptions.js | 17 - .../rocketchat-livestream/server/index.js | 2 - .../rocketchat-livestream/server/methods.js | 141 - .../server/models/Rooms.js | 8 - .../rocketchat-livestream/server/routes.js | 45 - .../rocketchat-livestream/server/settings.js | 22 - packages/rocketchat-logger/client/ansispan.js | 46 - packages/rocketchat-logger/client/logger.js | 84 - packages/rocketchat-logger/client/viewLogs.js | 25 - .../client/views/viewLogs.html | 18 - .../client/views/viewLogs.js | 125 - packages/rocketchat-logger/package.js | 28 - packages/rocketchat-logger/server/server.js | 377 - packages/rocketchat-mailer/client/router.js | 16 - packages/rocketchat-mailer/client/startup.js | 8 - .../rocketchat-mailer/client/views/mailer.js | 31 - .../client/views/mailerUnsubscribe.js | 3 - packages/rocketchat-mailer/lib/Mailer.js | 1 - packages/rocketchat-mailer/package.js | 39 - .../server/functions/sendMail.js | 82 - .../server/functions/unsubscribe.js | 7 - .../server/methods/sendMail.js | 25 - .../server/methods/unsubscribe.js | 14 - .../rocketchat-mailer/server/models/Users.js | 14 - packages/rocketchat-mailer/server/startup.js | 8 - packages/rocketchat-mapview/package.js | 17 - .../rocketchat-mapview/server/settings.js | 4 - packages/rocketchat-markdown/markdown.js | 84 - packages/rocketchat-markdown/package.js | 17 - .../parser/marked/marked.js | 108 - .../parser/original/markdown.js | 90 - packages/rocketchat-markdown/settings.js | 88 - .../rocketchat-markdown/tests/client.mocks.js | 45 - .../rocketchat-markdown/tests/client.tests.js | 281 - .../client/actionButton.js | 20 - .../client/lib/MentionedMessage.js | 1 - .../client/tabBar.js | 10 - .../client/views/mentionsFlexTab.html | 21 - .../client/views/mentionsFlexTab.js | 50 - .../views/stylesheets/mentionsFlexTab.less | 11 - .../rocketchat-mentions-flextab/package.js | 30 - .../server/publications/mentionedMessages.js | 32 - packages/rocketchat-mentions/Mentions.js | 77 - packages/rocketchat-mentions/client/client.js | 16 - packages/rocketchat-mentions/package.js | 17 - .../rocketchat-mentions/server/Mentions.js | 75 - .../methods/getUserMentionsByChannel.js | 19 - packages/rocketchat-mentions/server/server.js | 31 - .../rocketchat-mentions/tests/client.tests.js | 328 - .../rocketchat-mentions/tests/server.tests.js | 268 - .../client/messageAction.js | 8 - packages/rocketchat-message-action/package.js | 20 - .../client/messageAttachment.html | 165 - .../client/messageAttachment.js | 72 - .../rocketchat-message-attachments/package.js | 22 - .../client/actionButton.js | 29 - .../package.js | 25 - .../server/logger.js | 7 - .../server/unreadMessages.js | 45 - .../client/actionButton.js | 96 - .../client/lib/PinnedMessage.js | 1 - .../client/messageType.js | 7 - .../client/pinMessage.js | 38 - .../rocketchat-message-pin/client/tabBar.js | 16 - .../client/views/pinnedMessages.html | 22 - .../client/views/pinnedMessages.js | 52 - packages/rocketchat-message-pin/package.js | 33 - .../server/pinMessage.js | 140 - .../server/publications/pinnedMessages.js | 26 - .../rocketchat-message-pin/server/settings.js | 12 - .../server/startup/indexes.js | 9 - .../client/actionButton.js | 60 - .../client/lib/collections.js | 1 - .../client/messageType.js | 13 - .../client/page/snippetPage.js | 36 - .../client/router.js | 15 - .../client/snippetMessage.js | 25 - .../client/tabBar/tabBar.js | 16 - .../tabBar/views/snippetedMessages.html | 24 - .../client/tabBar/views/snippetedMessages.js | 31 - .../rocketchat-message-snippet/package.js | 52 - .../server/methods/snippetMessage.js | 43 - .../server/publications/snippetedMessage.js | 47 - .../publications/snippetedMessagesByRoom.js | 36 - .../server/requests.js | 62 - .../server/startup/settings.js | 13 - .../client/actionButton.js | 97 - .../client/lib/StarredMessage.js | 1 - .../client/starMessage.js | 20 - .../rocketchat-message-star/client/tabBar.js | 10 - .../client/views/starredMessages.html | 21 - .../client/views/starredMessages.js | 51 - packages/rocketchat-message-star/package.js | 33 - .../server/publications/starredMessages.js | 30 - .../server/settings.js | 7 - .../server/starMessage.js | 23 - .../server/startup/indexes.js | 9 - packages/rocketchat-migrations/package.js | 16 - packages/rocketchat-mongo-config/package.js | 15 + .../rocketchat-mongo-config/server/index.js | 8 + packages/rocketchat-nrr/package.js | 12 - .../.gitignore | 1 - .../admin/client/collection.js | 1 - .../admin/client/route.js | 22 - .../admin/client/startup.js | 8 - .../admin/client/views/oauthApps.html | 33 - .../admin/client/views/oauthApps.js | 18 - .../admin/server/methods/addOAuthApp.js | 24 - .../admin/server/methods/deleteOAuthApp.js | 13 - .../admin/server/methods/updateOAuthApp.js | 36 - .../admin/server/publications/oauthApps.js | 9 - .../oauth/server/default-services.js | 15 - .../package.js | 50 - .../server/models/OAuthApps.js | 15 - .../client/oembedAudioWidget.js | 10 - .../client/oembedFrameWidget.js | 10 - .../client/oembedImageWidget.js | 21 - .../client/oembedSandstormGrain.html | 11 - .../client/oembedSandstormGrain.js | 25 - .../client/oembedVideoWidget.js | 35 - .../client/oembedYoutubeWidget.js | 10 - packages/rocketchat-oembed/package.js | 47 - .../rocketchat-oembed/server/jumpToMessage.js | 45 - .../rocketchat-oembed/server/providers.js | 118 - packages/rocketchat-oembed/server/server.js | 295 - .../rocketchat-otr/client/rocketchat.otr.js | 104 - packages/rocketchat-otr/client/tabBar.js | 19 - .../rocketchat-otr/client/views/otrFlexTab.js | 79 - packages/rocketchat-otr/package.js | 34 - .../server/methods/deleteOldOTRMessages.js | 15 - .../server/methods/updateOTRAck.js | 8 - .../rocketchat-otr/server/models/Messages.js | 10 - packages/rocketchat-otr/server/settings.js | 7 - .../.npm/plugin/minifier-postcss/.gitignore | 1 - .../.npm/plugin/minifier-postcss/README | 7 - .../minifier-postcss/npm-shrinkwrap.json | 62 - packages/rocketchat-postcss/package.js | 25 - .../rocketchat-postcss/plugin/minify-css.js | 198 - .../client/tabBar.js | 10 - .../rocketchat-push-notifications/package.js | 27 - .../methods/saveNotificationSettings.js | 102 - .../server/models/Subscriptions.js | 276 - packages/rocketchat-reactions/client/init.js | 70 - .../client/methods/setReaction.js | 56 - packages/rocketchat-reactions/package.js | 23 - .../server/models/Messages.js | 7 - packages/rocketchat-reactions/setReaction.js | 89 - .../rocketchat-retention-policy/package.js | 25 - .../server/cronPruneMessages.js | 123 - .../server/startup/settings.js | 107 - .../client/powerboxListener.js | 28 - .../rocketchat-sandstorm/client/setPath.js | 12 - packages/rocketchat-sandstorm/package.js | 13 - .../rocketchat-sandstorm/server/events.js | 41 - packages/rocketchat-sandstorm/server/lib.js | 37 - .../rocketchat-sandstorm/server/powerbox.js | 48 - .../client/provider/result.html | 31 - .../client/provider/result.js | 84 - packages/rocketchat-search/package.js | 27 - .../rocketchat-search/server/events/events.js | 63 - packages/rocketchat-search/server/index.js | 12 - .../rocketchat-search/server/logger/logger.js | 2 - .../server/model/provider.js | 176 - .../rocketchat-setup-wizard/client/final.js | 51 - .../client/setupWizard.js | 359 - packages/rocketchat-setup-wizard/package.js | 19 - .../server/getSetupWizardParameters.js | 18 - .../client/slackbridge_import.client.js | 9 - packages/rocketchat-slackbridge/package.js | 21 - .../server/RocketAdapter.js | 476 - .../rocketchat-slackbridge/server/logger.js | 12 - .../rocketchat-slackbridge/server/settings.js | 89 - .../server/slackbridge.js | 95 - .../server/slackbridge_import.server.js | 64 - .../lenny.js | 18 - .../package.js | 20 - .../client/client.js | 4 - .../package.js | 19 - .../server/messages.js | 3 - .../server/server.js | 67 - .../client/client.js | 4 - .../package.js | 19 - .../server/server.js | 51 - .../rocketchat-slashcommands-help/package.js | 19 - .../rocketchat-slashcommands-help/server.js | 46 - .../client/hide.js | 4 - .../rocketchat-slashcommands-hide/package.js | 16 - .../server/hide.js | 61 - .../client/client.js | 4 - .../package.js | 19 - .../server/server.js | 83 - .../client/client.js | 8 - .../package.js | 20 - .../server/server.js | 88 - .../client/client.js | 10 - .../rocketchat-slashcommands-join/package.js | 20 - .../server/server.js | 42 - .../client/client.js | 10 - .../rocketchat-slashcommands-kick/package.js | 19 - .../server/server.js | 47 - .../rocketchat-slashcommands-leave/leave.js | 24 - .../rocketchat-slashcommands-leave/package.js | 15 - packages/rocketchat-slashcommands-me/me.js | 20 - .../rocketchat-slashcommands-me/package.js | 15 - .../rocketchat-slashcommands-msg/package.js | 18 - .../rocketchat-slashcommands-msg/server.js | 49 - .../rocketchat-slashcommands-mute/package.js | 16 - .../server/mute.js | 50 - .../server/unmute.js | 48 - .../client/client.js | 48 - .../rocketchat-slashcommands-open/package.js | 20 - .../rocketchat-slashcommands-topic/package.js | 18 - .../rocketchat-slashcommands-topic/topic.js | 29 - .../client/client.js | 4 - .../package.js | 20 - .../server/messages.js | 3 - .../server/server.js | 68 - packages/rocketchat-slider/package.js | 16 - .../lib/rocketchat.js | 1 - .../rocketchat-smarsh-connector/package.js | 22 - .../server/functions/sendEmail.js | 34 - .../server/models/SmarshHistory.js | 5 - .../server/settings.js | 61 - .../server/startup.js | 29 - packages/rocketchat-sms/SMS.js | 23 - packages/rocketchat-sms/package.js | 16 - packages/rocketchat-sms/settings.js | 36 - packages/rocketchat-spotify/package.js | 20 - .../rocketchat-statistics/lib/rocketchat.js | 1 - packages/rocketchat-statistics/package.js | 23 - .../server/functions/get.js | 104 - .../server/functions/save.js | 6 - .../server/methods/getStatistics.js | 17 - .../client/imports/components/badge.css | 21 - .../imports/components/contextual-bar.css | 306 - .../client/imports/components/header.css | 429 - .../client/imports/components/message-box.css | 322 - .../client/imports/components/messages.css | 120 - .../components/modal/create-channel.css | 70 - .../imports/components/modal/directory.css | 74 - .../client/imports/components/tabs.css | 36 - .../client/imports/forms/button.css | 159 - .../client/imports/general/apps.css | 132 - .../client/imports/general/rtl.css | 784 - .../client/imports/general/variables.css | 329 - packages/rocketchat-theme/package.js | 42 - packages/rocketchat-theme/server/server.js | 185 - .../client/login_token_client.js | 20 - packages/rocketchat-token-login/package.js | 23 - .../rocketchat-tokenpass/client/roomType.js | 21 - .../rocketchat-tokenpass/client/startup.js | 16 - .../client/tokenChannelsList.js | 24 - packages/rocketchat-tokenpass/common.js | 36 - packages/rocketchat-tokenpass/package.js | 51 - .../server/cronRemoveUsers.js | 45 - .../getProtectedTokenpassBalances.js | 19 - .../functions/getPublicTokenpassBalances.js | 19 - .../server/functions/saveRoomTokens.js | 9 - .../functions/saveRoomTokensMinimumBalance.js | 13 - .../functions/updateUserTokenpassBalances.js | 14 - .../server/methods/findTokenChannels.js | 21 - .../server/methods/getChannelTokenpass.js | 17 - .../server/models/Rooms.js | 41 - .../server/models/Subscriptions.js | 15 - .../server/models/Users.js | 23 - .../server/models/indexes.js | 3 - .../rocketchat-tokenpass/server/startup.js | 48 - packages/rocketchat-tooltip/client/init.js | 3 - packages/rocketchat-tooltip/package.js | 22 - packages/rocketchat-tutum/package.js | 11 - packages/rocketchat-tutum/startup.js | 25 - .../rocketchat-ui-account/client/account.js | 6 - .../client/accountFlex.js | 30 - .../client/accountPreferences.html | 340 - .../client/accountPreferences.js | 326 - .../client/avatar/avatar.js | 23 - packages/rocketchat-ui-account/package.js | 37 - .../client/SettingsCachedCollection.js | 32 - packages/rocketchat-ui-admin/client/admin.js | 609 - .../rocketchat-ui-admin/client/adminFlex.js | 85 - .../client/rooms/adminRooms.html | 62 - .../client/rooms/adminRooms.js | 152 - .../client/users/adminUserChannels.js | 27 - .../client/users/adminUserInfo.html | 12 - .../client/users/adminUsers.html | 66 - .../client/users/adminUsers.js | 135 - packages/rocketchat-ui-admin/package.js | 51 - .../publications/adminRooms.js | 48 - .../client/lib/startup.js | 12 - .../rocketchat-ui-clean-history/package.js | 27 - packages/rocketchat-ui-flextab/client/app.js | 0 .../client/flexTabBar.js | 231 - .../client/tabs/membersList.html | 60 - .../client/tabs/membersList.js | 310 - .../client/tabs/uploadedFilesList.js | 119 - .../client/tabs/userActions.js | 482 - .../client/tabs/userEdit.html | 120 - .../client/tabs/userEdit.js | 179 - .../client/tabs/userInfo.js | 286 - packages/rocketchat-ui-flextab/package.js | 35 - .../client/login/form.html | 156 - .../rocketchat-ui-login/client/login/form.js | 301 - .../client/login/header.js | 9 - .../client/login/layout.js | 13 - .../client/login/services.js | 105 - packages/rocketchat-ui-login/client/routes.js | 6 - .../client/username/layout.js | 9 - packages/rocketchat-ui-login/package.js | 47 - .../rocketchat-ui-master/client/main.html | 72 - packages/rocketchat-ui-master/client/main.js | 234 - packages/rocketchat-ui-master/package.js | 32 - .../rocketchat-ui-master/public/icons.svg | 845 - .../rocketchat-ui-master/server/inject.js | 146 - .../rocketchat-ui-message/client/message.html | 140 - .../rocketchat-ui-message/client/message.js | 434 - .../client/messageBox.html | 149 - .../client/messageBox.js | 686 - .../client/messageDropdown.html | 12 - .../client/popup/messagePopupChannel.js | 5 - .../client/popup/messagePopupConfig.html | 11 - .../client/popup/messagePopupConfig.js | 449 - .../client/popup/messagePopupEmoji.js | 6 - .../client/renderMessageBody.js | 23 - packages/rocketchat-ui-message/package.js | 48 - .../startup/messageBoxActions.js | 86 - .../client/chatRoomItem.js | 56 - .../client/createCombinedFlex.html | 69 - .../client/createCombinedFlex.js | 153 - .../client/listChannelsFlex.html | 63 - .../client/listChannelsFlex.js | 124 - .../client/listCombinedFlex.html | 71 - .../client/listCombinedFlex.js | 144 - .../client/listPrivateGroupsFlex.html | 49 - .../client/listPrivateGroupsFlex.js | 80 - .../rocketchat-ui-sidenav/client/roomList.js | 145 - .../rocketchat-ui-sidenav/client/sideNav.html | 30 - .../rocketchat-ui-sidenav/client/sideNav.js | 88 - .../client/sidebarHeader.js | 308 - .../client/sidebarItem.html | 74 - .../client/sidebarItem.js | 207 - .../rocketchat-ui-sidenav/client/sortlist.js | 43 - .../rocketchat-ui-sidenav/client/toolbar.js | 245 - packages/rocketchat-ui-sidenav/package.js | 46 - .../client/VRecDialog.js | 81 - .../rocketchat-ui-vrecord/client/vrecord.js | 48 - packages/rocketchat-ui-vrecord/package.js | 25 - .../rocketchat-ui-vrecord/server/settings.js | 7 - .../client/components/contextualBar.html | 22 - .../client/components/contextualBar.js | 26 - .../client/components/header/header.html | 82 - .../client/components/header/header.js | 147 - .../rocketchat-ui/client/components/icon.js | 7 - .../client/components/popupList.html | 37 - .../client/components/popupList.js | 37 - .../client/components/selectDropdown.html | 19 - .../client/components/selectDropdown.js | 20 - .../rocketchat-ui/client/components/tabs.html | 9 - .../rocketchat-ui/client/components/tabs.js | 23 - .../client/lib/RoomHistoryManager.js | 277 - .../rocketchat-ui/client/lib/RoomManager.js | 317 - .../rocketchat-ui/client/lib/accountBox.js | 94 - packages/rocketchat-ui/client/lib/accounts.js | 12 - packages/rocketchat-ui/client/lib/avatar.js | 35 - .../rocketchat-ui/client/lib/chatMessages.js | 586 - .../rocketchat-ui/client/lib/collections.js | 27 - .../client/lib/cordova/facebook-login.js | 31 - .../client/lib/cordova/keyboard-fix.js | 32 - .../rocketchat-ui/client/lib/cordova/push.js | 89 - .../rocketchat-ui/client/lib/cordova/urls.js | 17 - .../client/lib/cordova/user-state.js | 26 - packages/rocketchat-ui/client/lib/esc.js | 58 - .../rocketchat-ui/client/lib/fireEvent.js | 18 - packages/rocketchat-ui/client/lib/menu.js | 240 - packages/rocketchat-ui/client/lib/modal.js | 108 - .../rocketchat-ui/client/lib/msgTyping.js | 84 - .../rocketchat-ui/client/lib/notification.js | 172 - .../client/lib/recorderjs/audioRecorder.js | 54 - .../client/lib/recorderjs/recorder.js | 93 - .../client/lib/recorderjs/videoRecorder.js | 100 - packages/rocketchat-ui/client/lib/sideNav.js | 129 - packages/rocketchat-ui/client/lib/tapi18n.js | 28 - .../client/lib/textarea-autogrow.js | 130 - .../textarea-cursor/set-cursor-position.js | 17 - .../client/views/404/roomNotFound.js | 18 - .../rocketchat-ui/client/views/app/burger.js | 13 - .../client/views/app/createChannel.js | 412 - .../client/views/app/directory.html | 121 - .../client/views/app/directory.js | 223 - .../client/views/app/fullModal.js | 33 - .../rocketchat-ui/client/views/app/helpers.js | 19 - .../rocketchat-ui/client/views/app/home.js | 8 - .../rocketchat-ui/client/views/app/modal.html | 61 - .../rocketchat-ui/client/views/app/modal.js | 157 - .../rocketchat-ui/client/views/app/room.html | 157 - .../rocketchat-ui/client/views/app/room.js | 1143 - .../client/views/app/roomSearch.js | 15 - .../client/views/app/secretURL.js | 29 - .../client/views/app/videoCall/videoCall.html | 63 - .../client/views/app/videoCall/videoCall.js | 129 - .../rocketchat-ui/client/views/cmsPage.html | 8 - .../rocketchat-ui/client/views/cmsPage.js | 25 - packages/rocketchat-ui/client/views/modal.js | 1 - .../rocketchat-ui/getAvatarUrlFromUsername.js | 17 - packages/rocketchat-ui/package.js | 147 - .../rocketchat-user-data-download/package.js | 18 - .../server/startup/settings.js | 34 - .../rocketchat-version-check/client/client.js | 28 - packages/rocketchat-version-check/package.js | 19 - .../server/addSettings.js | 8 - .../server/checkUpdate.js | 14 - .../server/functions/checkVersionUpdate.js | 54 - .../server/functions/getNewUpdates.js | 41 - .../rocketchat-version-check/server/logger.js | 1 - .../server/methods/banner_dismiss.js | 11 - .../rocketchat-version-check/server/server.js | 29 - .../plugin/compile-version.js | 12 +- .../client/actionLink.js | 16 - .../rocketchat-videobridge/client/tabBar.js | 83 - .../client/views/videoFlexTab.js | 144 - .../client/views/videoFlexTabBbb.js | 63 - .../rocketchat-videobridge/lib/messageType.js | 7 - packages/rocketchat-videobridge/package.js | 38 - .../server/actionLink.js | 3 - .../server/methods/jitsiSetTimeout.js | 33 - .../server/models/Rooms.js | 18 - .../rocketchat-videobridge/server/settings.js | 132 - packages/rocketchat-webrtc/package.js | 21 - packages/rocketchat-webrtc/server/settings.js | 22 - packages/rocketchat-wordpress/common.js | 95 - packages/rocketchat-wordpress/package.js | 17 - packages/rocketchat-wordpress/startup.js | 86 - packages/tap-i18n/.gitignore | 7 - .../.npm/plugin/tap-i18n-compiler}/.gitignore | 0 .../.npm/plugin/tap-i18n-compiler}/README | 0 .../tap-i18n-compiler/npm-shrinkwrap.json | 70 + packages/tap-i18n/LICENSE | 21 - packages/tap-i18n/README.md | 808 - packages/tap-i18n/TODO | 3 - packages/tap-i18n/lib/globals.js | 13 - .../lib/plugin/compiler_configuration.coffee | 23 - .../tap-i18n/lib/plugin/compilers/i18n.coffee | 18 - .../compilers/i18n.generic_compiler.coffee | 124 - .../lib/plugin/compilers/i18n.json.coffee | 4 - .../lib/plugin/compilers/i18n.yml.coffee | 4 - .../plugin/compilers/package-tap.i18n.coffee | 95 - .../plugin/compilers/project-tap.i18n.coffee | 122 - .../lib/plugin/compilers/share.coffee | 1 - .../tap-i18n/lib/plugin/etc/language_names.js | 141 - .../helpers/compile_step_helpers.coffee | 23 - .../lib/plugin/helpers/helpers.coffee | 1 - .../lib/plugin/helpers/load_json.coffee | 24 - .../lib/plugin/helpers/load_yml.coffee | 25 - .../lib/tap_i18n/tap_i18n-client.coffee | 198 - .../lib/tap_i18n/tap_i18n-common.coffee | 143 - .../lib/tap_i18n/tap_i18n-helpers.coffee | 1 - .../lib/tap_i18n/tap_i18n-init.coffee | 1 - .../lib/tap_i18n/tap_i18n-server.coffee | 85 - .../lib/tap_i18next/tap_i18next-1.7.3.js | 2748 -- .../lib/tap_i18next/tap_i18next_init.js | 3 - packages/tap-i18n/package.js | 79 - private/client/imports/general/variables.css | 378 + private/node_scripts/auto-translate.js | 13 +- private/node_scripts/check-unused-i18n.js | 1 + .../unsubscribe_csv/unsubscribe.js | 6 +- private/public/icons.svg | 374 + .../server/asset/chatpal-enter.svg | 0 .../asset/chatpal-logo-icon-darkblue.svg | 0 .../server/colors.less | 112 +- .../server/dynamic-css.js | 7 +- .../client/vendor/fontello/demo.html | 0 .../client/vendor/fontello/utf8-rtl.html | 0 .../fontello => public}/font/fontello.eot | Bin .../fontello => public}/font/fontello.svg | 0 .../fontello => public}/font/fontello.ttf | Bin .../fontello => public}/font/fontello.woff | Bin .../fontello => public}/font/fontello.woff2 | Bin public/fonts/KaTeX_AMS-Regular.ttf | Bin 0 -> 71428 bytes public/fonts/KaTeX_AMS-Regular.woff | Bin 0 -> 40200 bytes public/fonts/KaTeX_AMS-Regular.woff2 | Bin 0 -> 33208 bytes public/fonts/KaTeX_Caligraphic-Bold.ttf | Bin 0 -> 19588 bytes public/fonts/KaTeX_Caligraphic-Bold.woff | Bin 0 -> 12136 bytes public/fonts/KaTeX_Caligraphic-Bold.woff2 | Bin 0 -> 10608 bytes public/fonts/KaTeX_Caligraphic-Regular.ttf | Bin 0 -> 18960 bytes public/fonts/KaTeX_Caligraphic-Regular.woff | Bin 0 -> 11868 bytes public/fonts/KaTeX_Caligraphic-Regular.woff2 | Bin 0 -> 10412 bytes public/fonts/KaTeX_Fraktur-Bold.ttf | Bin 0 -> 35968 bytes public/fonts/KaTeX_Fraktur-Bold.woff | Bin 0 -> 23388 bytes public/fonts/KaTeX_Fraktur-Bold.woff2 | Bin 0 -> 20488 bytes public/fonts/KaTeX_Fraktur-Regular.ttf | Bin 0 -> 34652 bytes public/fonts/KaTeX_Fraktur-Regular.woff | Bin 0 -> 22844 bytes public/fonts/KaTeX_Fraktur-Regular.woff2 | Bin 0 -> 19868 bytes public/fonts/KaTeX_Main-Bold.ttf | Bin 0 -> 61772 bytes public/fonts/KaTeX_Main-Bold.woff | Bin 0 -> 36928 bytes public/fonts/KaTeX_Main-Bold.woff2 | Bin 0 -> 30824 bytes public/fonts/KaTeX_Main-BoldItalic.ttf | Bin 0 -> 44824 bytes public/fonts/KaTeX_Main-BoldItalic.woff | Bin 0 -> 26228 bytes public/fonts/KaTeX_Main-BoldItalic.woff2 | Bin 0 -> 22176 bytes public/fonts/KaTeX_Main-Italic.ttf | Bin 0 -> 47952 bytes public/fonts/KaTeX_Main-Italic.woff | Bin 0 -> 27200 bytes public/fonts/KaTeX_Main-Italic.woff2 | Bin 0 -> 23064 bytes public/fonts/KaTeX_Main-Regular.ttf | Bin 0 -> 70512 bytes public/fonts/KaTeX_Main-Regular.woff | Bin 0 -> 39728 bytes public/fonts/KaTeX_Main-Regular.woff2 | Bin 0 -> 33096 bytes public/fonts/KaTeX_Math-Italic.ttf | Bin 0 -> 41448 bytes public/fonts/KaTeX_Math-Italic.woff | Bin 0 -> 23820 bytes public/fonts/KaTeX_Math-Italic.woff2 | Bin 0 -> 20416 bytes public/fonts/KaTeX_SansSerif-Bold.ttf | Bin 0 -> 34032 bytes public/fonts/KaTeX_SansSerif-Bold.woff | Bin 0 -> 19172 bytes public/fonts/KaTeX_SansSerif-Bold.woff2 | Bin 0 -> 16000 bytes public/fonts/KaTeX_SansSerif-Italic.ttf | Bin 0 -> 31308 bytes public/fonts/KaTeX_SansSerif-Italic.woff | Bin 0 -> 18128 bytes public/fonts/KaTeX_SansSerif-Italic.woff2 | Bin 0 -> 15204 bytes public/fonts/KaTeX_SansSerif-Regular.ttf | Bin 0 -> 30156 bytes public/fonts/KaTeX_SansSerif-Regular.woff | Bin 0 -> 16784 bytes public/fonts/KaTeX_SansSerif-Regular.woff2 | Bin 0 -> 14004 bytes public/fonts/KaTeX_Script-Regular.ttf | Bin 0 -> 24864 bytes public/fonts/KaTeX_Script-Regular.woff | Bin 0 -> 13856 bytes public/fonts/KaTeX_Script-Regular.woff2 | Bin 0 -> 12268 bytes public/fonts/KaTeX_Size1-Regular.ttf | Bin 0 -> 13172 bytes public/fonts/KaTeX_Size1-Regular.woff | Bin 0 -> 6980 bytes public/fonts/KaTeX_Size1-Regular.woff2 | Bin 0 -> 5812 bytes public/fonts/KaTeX_Size2-Regular.ttf | Bin 0 -> 12412 bytes public/fonts/KaTeX_Size2-Regular.woff | Bin 0 -> 6684 bytes public/fonts/KaTeX_Size2-Regular.woff2 | Bin 0 -> 5560 bytes public/fonts/KaTeX_Size3-Regular.ttf | Bin 0 -> 8360 bytes public/fonts/KaTeX_Size3-Regular.woff | Bin 0 -> 4776 bytes public/fonts/KaTeX_Size3-Regular.woff2 | Bin 0 -> 3860 bytes public/fonts/KaTeX_Size4-Regular.ttf | Bin 0 -> 11284 bytes public/fonts/KaTeX_Size4-Regular.woff | Bin 0 -> 6456 bytes public/fonts/KaTeX_Size4-Regular.woff2 | Bin 0 -> 5132 bytes public/fonts/KaTeX_Typewriter-Regular.ttf | Bin 0 -> 36308 bytes public/fonts/KaTeX_Typewriter-Regular.woff | Bin 0 -> 20924 bytes public/fonts/KaTeX_Typewriter-Regular.woff2 | Bin 0 -> 17536 bytes public/images/404.svg | 323 + public/images/logo/logo.png | Bin 0 -> 17921 bytes public/lame.min.js | 614 +- public/mp3-realtime-worker.js | 72 +- public/packages/emojione/activity-sprites.png | Bin 0 -> 837136 bytes public/packages/emojione/flags-sprites.png | Bin 0 -> 465145 bytes public/packages/emojione/food-sprites.png | Bin 0 -> 413969 bytes public/packages/emojione/modifier-sprites.png | Bin 0 -> 4358 bytes public/packages/emojione/nature-sprites.png | Bin 0 -> 659156 bytes public/packages/emojione/objects-sprites.png | Bin 0 -> 552504 bytes public/packages/emojione/people-sprites.png | Bin 0 -> 3452500 bytes public/packages/emojione/regional-sprites.png | Bin 0 -> 20555 bytes public/packages/emojione/symbols-sprites.png | Bin 0 -> 478409 bytes public/packages/emojione/travel-sprites.png | Bin 0 -> 340248 bytes .../client/public/external_api.js | 0 public/pdf.worker.min.js | 1 + public/public/icons.html | 398 + server/configuration/accounts_meld.js | 14 +- server/importPackages.js | 120 + server/lib/accounts.js | 129 +- server/lib/cordova.js | 57 +- server/lib/cordova/facebook-login.js | 40 - server/lib/roomFiles.js | 14 +- server/main.js | 84 +- server/methods/OEmbedCacheCleanup.js | 12 +- server/methods/addAllUserToRoom.js | 40 +- server/methods/addRoomLeader.js | 24 +- server/methods/addRoomModerator.js | 24 +- server/methods/addRoomOwner.js | 24 +- server/methods/afterVerifyEmail.js | 21 +- server/methods/browseChannels.js | 120 +- server/methods/canAccessRoom.js | 40 +- server/methods/channelsList.js | 37 +- server/methods/createDirectMessage.js | 52 +- server/methods/deleteFileMessage.js | 9 +- server/methods/deleteUser.js | 17 +- server/methods/eraseRoom.js | 28 +- server/methods/getAvatarSuggestion.js | 116 +- server/methods/getRoomById.js | 40 + server/methods/getRoomIdByNameOrId.js | 10 +- server/methods/getRoomNameById.js | 12 +- server/methods/getTotalChannels.js | 6 +- server/methods/getUsernameSuggestion.js | 122 - server/methods/getUsersOfRoom.js | 70 +- server/methods/hideRoom.js | 7 +- server/methods/ignoreUser.js | 11 +- server/methods/loadHistory.js | 21 +- server/methods/loadLocale.js | 5 +- server/methods/loadMissedMessages.js | 10 +- server/methods/loadNextMessages.js | 22 +- server/methods/loadSurroundingMessages.js | 34 +- server/methods/logoutCleanUp.js | 7 +- server/methods/messageSearch.js | 17 +- server/methods/migrate.js | 12 +- server/methods/muteUserInRoom.js | 27 +- server/methods/openRoom.js | 7 +- server/methods/readMessages.js | 15 +- server/methods/registerUser.js | 69 +- server/methods/removeRoomLeader.js | 24 +- server/methods/removeRoomModerator.js | 24 +- server/methods/removeRoomOwner.js | 26 +- server/methods/removeUserFromRoom.js | 35 +- server/methods/reportMessage.js | 9 +- server/methods/requestDataDownload.js | 44 +- server/methods/resetAvatar.js | 38 +- server/methods/roomNameExists.js | 7 +- server/methods/saveUserPreferences.js | 26 +- server/methods/saveUserProfile.js | 26 +- server/methods/sendConfirmationEmail.js | 76 +- server/methods/sendForgotPasswordEmail.js | 97 +- server/methods/setAvatarFromService.js | 36 +- server/methods/setUserActiveStatus.js | 65 +- server/methods/setUserPassword.js | 13 +- server/methods/toogleFavorite.js | 9 +- server/methods/unmuteUserInRoom.js | 27 +- server/methods/userSetUtcOffset.js | 10 +- server/publications/activeUsers.js | 7 +- .../channelAndPrivateAutocomplete.js | 9 +- server/publications/fullUserData.js | 25 +- server/publications/messages.js | 27 +- server/publications/room.js | 132 - server/publications/room/emitter.js | 37 + server/publications/room/index.js | 109 + server/publications/roomFiles.js | 2 + .../publications/roomFilesWithSearchText.js | 2 + .../publications/roomSubscriptionsByRole.js | 11 +- server/publications/settings/emitter.js | 45 + server/publications/settings/index.js | 55 + server/publications/spotlight.js | 38 +- server/publications/subscription.js | 83 - server/publications/subscription/emitter.js | 27 + server/publications/subscription/index.js | 72 + server/publications/userAutocomplete.js | 5 +- server/publications/userChannels.js | 9 +- server/publications/userData.js | 43 +- server/routes/avatar/index.js | 9 + server/routes/avatar/middlewares/auth.js | 15 + server/routes/avatar/middlewares/index.js | 5 + server/routes/avatar/room.js | 41 + server/routes/avatar/user.js | 77 + server/routes/avatar/utils.js | 96 + server/startup/appcache.js | 2 + server/startup/avatar.js | 159 - server/startup/cron.js | 25 +- server/startup/i18n-validation.js | 67 - server/startup/initialData.js | 92 +- server/startup/migrations/index.js | 150 + server/startup/migrations/v001.js | 15 +- server/startup/migrations/v002.js | 12 +- server/startup/migrations/v003.js | 38 +- server/startup/migrations/v004.js | 28 +- server/startup/migrations/v005.js | 69 +- server/startup/migrations/v006.js | 15 +- server/startup/migrations/v007.js | 9 +- server/startup/migrations/v008.js | 19 +- server/startup/migrations/v009.js | 18 +- server/startup/migrations/v010.js | 9 +- server/startup/migrations/v011.js | 7 +- server/startup/migrations/v012.js | 11 +- server/startup/migrations/v013.js | 7 +- server/startup/migrations/v014.js | 29 +- server/startup/migrations/v015.js | 16 +- server/startup/migrations/v016.js | 7 +- server/startup/migrations/v017.js | 7 +- server/startup/migrations/v018.js | 13 +- server/startup/migrations/v019.js | 26 +- server/startup/migrations/v020.js | 9 +- server/startup/migrations/v021.js | 7 +- server/startup/migrations/v022.js | 7 +- server/startup/migrations/v023.js | 7 +- server/startup/migrations/v024.js | 7 +- server/startup/migrations/v025.js | 7 +- server/startup/migrations/v026.js | 7 +- server/startup/migrations/v027.js | 14 +- server/startup/migrations/v028.js | 7 +- server/startup/migrations/v029.js | 49 +- server/startup/migrations/v030.js | 25 +- server/startup/migrations/v031.js | 11 +- server/startup/migrations/v032.js | 7 +- server/startup/migrations/v033.js | 13 +- server/startup/migrations/v034.js | 7 +- server/startup/migrations/v035.js | 7 +- server/startup/migrations/v036.js | 15 +- server/startup/migrations/v037.js | 14 +- server/startup/migrations/v038.js | 14 +- server/startup/migrations/v039.js | 11 +- server/startup/migrations/v040.js | 11 +- server/startup/migrations/v041.js | 13 +- server/startup/migrations/v042.js | 20 +- server/startup/migrations/v043.js | 9 +- server/startup/migrations/v044.js | 27 +- server/startup/migrations/v045.js | 11 +- server/startup/migrations/v046.js | 9 +- server/startup/migrations/v047.js | 17 +- server/startup/migrations/v048.js | 21 +- server/startup/migrations/v049.js | 14 +- server/startup/migrations/v050.js | 9 +- server/startup/migrations/v051.js | 13 +- server/startup/migrations/v052.js | 7 +- server/startup/migrations/v053.js | 9 +- server/startup/migrations/v054.js | 11 +- server/startup/migrations/v055.js | 11 +- server/startup/migrations/v056.js | 11 +- server/startup/migrations/v057.js | 25 +- server/startup/migrations/v058.js | 7 +- server/startup/migrations/v059.js | 11 +- server/startup/migrations/v060.js | 15 +- server/startup/migrations/v061.js | 9 +- server/startup/migrations/v062.js | 7 +- server/startup/migrations/v063.js | 17 +- server/startup/migrations/v064.js | 9 +- server/startup/migrations/v065.js | 66 +- server/startup/migrations/v066.js | 12 +- server/startup/migrations/v067.js | 9 +- server/startup/migrations/v068.js | 12 +- server/startup/migrations/v069.js | 23 +- server/startup/migrations/v070.js | 11 +- server/startup/migrations/v071.js | 9 +- server/startup/migrations/v072.js | 9 +- server/startup/migrations/v073.js | 9 +- server/startup/migrations/v074.js | 17 +- server/startup/migrations/v075.js | 9 +- server/startup/migrations/v076.js | 11 +- server/startup/migrations/v077.js | 14 +- server/startup/migrations/v078.js | 7 +- server/startup/migrations/v079.js | 9 +- server/startup/migrations/v080.js | 7 +- server/startup/migrations/v081.js | 7 +- server/startup/migrations/v082.js | 9 +- server/startup/migrations/v083.js | 13 +- server/startup/migrations/v084.js | 23 +- server/startup/migrations/v085.js | 11 +- server/startup/migrations/v086.js | 4 +- server/startup/migrations/v087.js | 15 +- server/startup/migrations/v088.js | 11 +- server/startup/migrations/v089.js | 13 +- server/startup/migrations/v090.js | 11 +- server/startup/migrations/v091.js | 9 +- server/startup/migrations/v092.js | 9 +- server/startup/migrations/v093.js | 14 +- server/startup/migrations/v094.js | 13 +- server/startup/migrations/v095.js | 15 +- server/startup/migrations/v096.js | 9 +- server/startup/migrations/v097.js | 4 +- server/startup/migrations/v098.js | 7 +- server/startup/migrations/v099.js | 44 +- server/startup/migrations/v100.js | 7 +- server/startup/migrations/v101.js | 7 +- server/startup/migrations/v102.js | 71 +- server/startup/migrations/v103.js | 21 +- server/startup/migrations/v104.js | 29 +- server/startup/migrations/v105.js | 51 +- server/startup/migrations/v106.js | 7 +- server/startup/migrations/v107.js | 15 +- server/startup/migrations/v108.js | 11 +- server/startup/migrations/v109.js | 21 +- server/startup/migrations/v110.js | 75 +- server/startup/migrations/v111.js | 11 +- server/startup/migrations/v112.js | 37 +- server/startup/migrations/v113.js | 15 +- server/startup/migrations/v114.js | 23 +- server/startup/migrations/v115.js | 7 +- server/startup/migrations/v116.js | 21 +- server/startup/migrations/v117.js | 13 +- server/startup/migrations/v118.js | 25 +- server/startup/migrations/v119.js | 11 +- server/startup/migrations/v120.js | 13 +- server/startup/migrations/v121.js | 14 +- server/startup/migrations/v122.js | 7 +- server/startup/migrations/v123.js | 23 +- server/startup/migrations/v124.js | 9 +- server/startup/migrations/v125.js | 7 +- server/startup/migrations/v126.js | 13 +- server/startup/migrations/v127.js | 17 +- server/startup/migrations/v128.js | 7 +- server/startup/migrations/v129.js | 13 +- server/startup/migrations/v130.js | 20 +- server/startup/migrations/v131.js | 37 +- server/startup/migrations/v132.js | 11 +- server/startup/migrations/v133.js | 39 + server/startup/migrations/v134.js | 66 + server/startup/migrations/v135.js | 26 + server/startup/migrations/v136.js | 19 + server/startup/migrations/v137.js | 14 + server/startup/migrations/v138.js | 15 + server/startup/migrations/v139.js | 118 + server/startup/migrations/v140.js | 16 + server/startup/migrations/v141.js | 9 + server/startup/migrations/v142.js | 9 + server/startup/migrations/v143.js | 21 + server/startup/migrations/v144.js | 24 + server/startup/migrations/v145.js | 17 + server/startup/migrations/v146.js | 23 + server/startup/migrations/v147.js | 22 + server/startup/migrations/v148.js | 30 + server/startup/migrations/v149.js | 22 + server/startup/migrations/xrun.js | 12 +- server/startup/presence.js | 11 +- server/startup/serverRunning.js | 95 +- server/stream/messages.js | 76 - server/stream/messages/emitter.js | 39 + server/stream/messages/index.js | 51 + server/stream/streamBroadcast.js | 69 +- tests/.eslintrc | 12 + tests/chimp-config.js | 74 +- tests/data/api-data.js | 13 +- tests/data/chat.helper.js | 34 + tests/data/permissions.helper.js | 19 + tests/data/rooms.helper.js | 44 + tests/data/users.helper.js | 25 + tests/end-to-end/api/00-miscellaneous.js | 56 +- tests/end-to-end/api/01-users.js | 586 +- tests/end-to-end/api/02-channels.js | 481 +- tests/end-to-end/api/03-groups.js | 408 +- tests/end-to-end/api/04-direct-message.js | 153 +- tests/end-to-end/api/05-chat.js | 1531 +- .../api/06-outgoing-integrations.js | 8 +- .../api/07-incoming-integrations.js | 2 - tests/end-to-end/api/08-settings.js | 3 - tests/end-to-end/api/09-rooms.js | 580 +- tests/end-to-end/api/10-subscriptions.js | 5 +- tests/end-to-end/api/11-graphql.js | 13 +- tests/end-to-end/api/11-permissions.js | 44 +- tests/end-to-end/api/12-emoji-custom.js | 279 +- tests/end-to-end/api/13-roles.js | 99 +- tests/end-to-end/api/14-assets.js | 4 - tests/end-to-end/api/15-video-conference.js | 51 + tests/end-to-end/ui/00-login.js | 4 +- tests/end-to-end/ui/01-register.js | 2 - tests/end-to-end/ui/02-forgot-password.js | 2 - tests/end-to-end/ui/03-user-creation.js | 5 - .../end-to-end/ui/04-main-elements-render.js | 13 +- tests/end-to-end/ui/05-channel-creation.js | 6 +- tests/end-to-end/ui/06-messaging.js | 9 +- tests/end-to-end/ui/07-emoji.js | 21 +- tests/end-to-end/ui/08-resolutions.js | 69 +- tests/end-to-end/ui/09-channel.js | 9 - tests/end-to-end/ui/10-user-preferences.js | 8 +- tests/end-to-end/ui/11-admin.js | 10 +- tests/end-to-end/ui/12-settings.js | 77 +- tests/end-to-end/ui/13-permissions.js | 5 - tests/end-to-end/ui/14-message-popup.js | 9 +- tests/end-to-end/ui/15-discussion.js | 63 + tests/pageobjects/Page.js | 4 +- tests/pageobjects/administration.page.js | 171 +- tests/pageobjects/discussion.page.js | 79 + tests/pageobjects/flex-tab.page.js | 73 +- tests/pageobjects/global.js | 8 + tests/pageobjects/keyboard.js | 20 + tests/pageobjects/login.page.js | 20 +- tests/pageobjects/main-content.page.js | 69 +- .../preferences-main-content.page.js | 9 + tests/pageobjects/settings.js | 46 + tests/pageobjects/setup-wizard.page.js | 12 + tests/pageobjects/side-nav.page.js | 82 +- 4133 files changed, 197497 insertions(+), 130843 deletions(-) delete mode 100644 .docker/Dockerfile.local delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/changelog.js delete mode 100755 .github/templates/commit.hbs delete mode 100755 .github/templates/footer.hbs delete mode 100755 .github/templates/header.hbs delete mode 100755 .github/templates/template.hbs delete mode 100644 .sandstorm/.gitignore delete mode 100644 .sandstorm/CHANGELOG.md delete mode 100644 .sandstorm/README.md delete mode 100644 .sandstorm/Vagrantfile delete mode 100755 .sandstorm/build.sh delete mode 100644 .sandstorm/description.md delete mode 100755 .sandstorm/global-setup.sh delete mode 100755 .sandstorm/launcher.sh delete mode 100644 .sandstorm/pgp-keyring delete mode 100644 .sandstorm/pgp-signature delete mode 100644 .sandstorm/rocket.chat-128.svg delete mode 100644 .sandstorm/rocket.chat-150.svg delete mode 100644 .sandstorm/rocket.chat-24.svg delete mode 100644 .sandstorm/sandstorm-pkgdef.capnp delete mode 100644 .sandstorm/screenshot1.png delete mode 100644 .sandstorm/screenshot2.png delete mode 100644 .sandstorm/screenshot3.png delete mode 100644 .sandstorm/screenshot4.png delete mode 100755 .sandstorm/setup.sh delete mode 100644 .sandstorm/stack create mode 100644 .scripts/fix-i18n.js create mode 100644 .scripts/npm-postinstall.js create mode 100644 .scripts/translationDiff.js create mode 100755 .snapcraft/snap/hooks/configure create mode 100755 .snapcraft/snap/hooks/install create mode 100755 .snapcraft/snap/hooks/post-refresh rename .snapcraft/{ => snap}/snapcraft.yaml (90%) delete mode 100755 .travis/sandstorm.sh create mode 100644 LIMITATION_OF_RESPONSIBILITY.md rename {packages/rocketchat-2fa => app/2fa}/README.md (100%) rename {packages/rocketchat-2fa => app/2fa}/client/TOTPPassword.js (90%) create mode 100644 app/2fa/client/accountSecurity.html rename {packages/rocketchat-2fa => app/2fa}/client/accountSecurity.js (92%) create mode 100644 app/2fa/client/index.js create mode 100644 app/2fa/server/index.js rename {packages/rocketchat-2fa => app/2fa}/server/lib/totp.js (78%) create mode 100644 app/2fa/server/loginHandler.js rename {packages/rocketchat-2fa => app/2fa}/server/methods/checkCodesRemaining.js (89%) create mode 100644 app/2fa/server/methods/disable.js create mode 100644 app/2fa/server/methods/enable.js create mode 100644 app/2fa/server/methods/regenerateCodes.js create mode 100644 app/2fa/server/methods/validateTempToken.js create mode 100644 app/2fa/server/startup/settings.js rename {packages/rocketchat-accounts => app/accounts}/README.md (100%) create mode 100644 app/accounts/index.js rename {packages/rocketchat-accounts => app/accounts}/server/config.js (100%) rename {packages/rocketchat-accounts => app/accounts}/server/index.js (100%) rename {packages/rocketchat-action-links => app/action-links}/README.md (100%) create mode 100644 app/action-links/both/lib/actionLinks.js create mode 100644 app/action-links/client/index.js create mode 100644 app/action-links/client/init.js create mode 100644 app/action-links/client/lib/actionLinks.js rename {packages/rocketchat-action-links => app/action-links}/client/stylesheets/actionLinks.css (100%) create mode 100644 app/action-links/index.js create mode 100644 app/action-links/server/actionLinkHandler.js create mode 100644 app/action-links/server/index.js rename {packages/rocketchat-analytics => app/analytics}/README.md (100%) create mode 100644 app/analytics/client/index.js rename {packages/rocketchat-analytics => app/analytics}/client/loadScript.js (76%) create mode 100644 app/analytics/client/trackEvents.js create mode 100644 app/analytics/server/index.js create mode 100644 app/analytics/server/settings.js create mode 100644 app/api/index.js create mode 100644 app/api/server/api.js create mode 100644 app/api/server/default/info.js rename {packages/rocketchat-api => app/api}/server/helpers/README.md (100%) create mode 100644 app/api/server/helpers/composeRoomWithLastMessage.js create mode 100644 app/api/server/helpers/deprecationWarning.js create mode 100644 app/api/server/helpers/getLoggedInUser.js create mode 100644 app/api/server/helpers/getPaginationItems.js create mode 100644 app/api/server/helpers/getUserFromParams.js create mode 100644 app/api/server/helpers/getUserInfo.js create mode 100644 app/api/server/helpers/insertUserObject.js create mode 100644 app/api/server/helpers/isUserFromParams.js create mode 100644 app/api/server/helpers/parseJsonQuery.js create mode 100644 app/api/server/helpers/requestParams.js create mode 100644 app/api/server/index.js create mode 100644 app/api/server/settings.js create mode 100644 app/api/server/v1/assets.js create mode 100644 app/api/server/v1/channels.js create mode 100644 app/api/server/v1/chat.js create mode 100644 app/api/server/v1/commands.js create mode 100644 app/api/server/v1/e2e.js create mode 100644 app/api/server/v1/emoji-custom.js create mode 100644 app/api/server/v1/groups.js create mode 100644 app/api/server/v1/im.js create mode 100644 app/api/server/v1/import.js create mode 100644 app/api/server/v1/integrations.js create mode 100644 app/api/server/v1/misc.js create mode 100644 app/api/server/v1/permissions.js create mode 100644 app/api/server/v1/push.js create mode 100644 app/api/server/v1/roles.js create mode 100644 app/api/server/v1/rooms.js create mode 100644 app/api/server/v1/settings.js create mode 100644 app/api/server/v1/stats.js create mode 100644 app/api/server/v1/subscriptions.js create mode 100644 app/api/server/v1/users.js create mode 100644 app/api/server/v1/video-conference.js rename {packages/rocketchat-apps => app/apps}/.gitignore (100%) rename {packages/rocketchat-apps => app/apps}/README.md (100%) create mode 100644 app/apps/assets/stylesheets/apps.css create mode 100644 app/apps/client/admin/appInstall.html rename {packages/rocketchat-apps => app/apps}/client/admin/appInstall.js (83%) create mode 100644 app/apps/client/admin/appLogs.html create mode 100644 app/apps/client/admin/appLogs.js create mode 100644 app/apps/client/admin/appManage.css create mode 100644 app/apps/client/admin/appManage.html create mode 100644 app/apps/client/admin/appManage.js rename {packages/rocketchat-apps => app/apps}/client/admin/appWhatIsIt.html (100%) create mode 100644 app/apps/client/admin/appWhatIsIt.js create mode 100644 app/apps/client/admin/apps.html create mode 100644 app/apps/client/admin/apps.js create mode 100644 app/apps/client/admin/helpers.js create mode 100644 app/apps/client/admin/marketplace.html create mode 100644 app/apps/client/admin/marketplace.js create mode 100644 app/apps/client/admin/modalTemplates/iframeModal.html create mode 100644 app/apps/client/admin/modalTemplates/iframeModal.js create mode 100644 app/apps/client/communication/index.js create mode 100644 app/apps/client/communication/websockets.js create mode 100644 app/apps/client/i18n.js create mode 100644 app/apps/client/index.js create mode 100644 app/apps/client/orchestrator.js create mode 100644 app/apps/client/routes.js create mode 100644 app/apps/lib/misc/Utilities.js create mode 100644 app/apps/lib/misc/transformMappedData.js rename {packages/rocketchat-apps => app/apps}/server/bridges/activation.js (100%) create mode 100644 app/apps/server/bridges/api.js rename {packages/rocketchat-apps => app/apps}/server/bridges/bridges.js (84%) create mode 100644 app/apps/server/bridges/commands.js rename {packages/rocketchat-apps => app/apps}/server/bridges/details.js (100%) create mode 100644 app/apps/server/bridges/environmental.js create mode 100644 app/apps/server/bridges/http.js create mode 100644 app/apps/server/bridges/index.js create mode 100644 app/apps/server/bridges/internal.js create mode 100644 app/apps/server/bridges/listeners.js create mode 100644 app/apps/server/bridges/messages.js create mode 100644 app/apps/server/bridges/persistence.js create mode 100644 app/apps/server/bridges/rooms.js create mode 100644 app/apps/server/bridges/settings.js create mode 100644 app/apps/server/bridges/users.js rename {packages/rocketchat-apps => app/apps}/server/communication/index.js (100%) create mode 100644 app/apps/server/communication/methods.js create mode 100644 app/apps/server/communication/rest.js create mode 100644 app/apps/server/communication/websockets.js rename {packages/rocketchat-apps => app/apps}/server/converters/index.js (100%) create mode 100644 app/apps/server/converters/messages.js create mode 100644 app/apps/server/converters/rooms.js create mode 100644 app/apps/server/converters/settings.js create mode 100644 app/apps/server/converters/users.js create mode 100644 app/apps/server/cron.js create mode 100644 app/apps/server/index.js create mode 100644 app/apps/server/orchestrator.js create mode 100644 app/apps/server/storage/index.js rename {packages/rocketchat-apps => app/apps}/server/storage/logs-storage.js (100%) rename {packages/rocketchat-apps => app/apps}/server/storage/storage.js (87%) create mode 100644 app/apps/server/tests/messages.tests.js create mode 100644 app/apps/server/tests/mocks/data/messages.data.js create mode 100644 app/apps/server/tests/mocks/models/BaseModel.mock.js create mode 100644 app/apps/server/tests/mocks/models/Messages.mock.js create mode 100644 app/apps/server/tests/mocks/models/Rooms.mock.js create mode 100644 app/apps/server/tests/mocks/models/Users.mock.js create mode 100644 app/apps/server/tests/mocks/models/index.js create mode 100644 app/apps/server/tests/mocks/orchestrator.mock.js create mode 100644 app/assets/index.js create mode 100644 app/assets/server/assets.js create mode 100644 app/assets/server/index.js rename {packages/rocketchat-authorization => app/authorization}/README.md (100%) create mode 100644 app/authorization/client/hasPermission.js create mode 100644 app/authorization/client/hasRole.js create mode 100644 app/authorization/client/index.js create mode 100644 app/authorization/client/lib/ChatPermissions.js rename {packages/rocketchat-authorization => app/authorization}/client/requiresPermission.html (100%) create mode 100644 app/authorization/client/route.js create mode 100644 app/authorization/client/startup.js create mode 100644 app/authorization/client/stylesheets/permissions.css create mode 100644 app/authorization/client/usersNameChanged.js create mode 100644 app/authorization/client/views/permissions.html create mode 100644 app/authorization/client/views/permissions.js create mode 100644 app/authorization/client/views/permissionsRole.html create mode 100644 app/authorization/client/views/permissionsRole.js create mode 100644 app/authorization/index.js create mode 100644 app/authorization/server/functions/addUserRoles.js create mode 100644 app/authorization/server/functions/canAccessRoom.js create mode 100644 app/authorization/server/functions/canSendMessage.js create mode 100644 app/authorization/server/functions/getRoles.js create mode 100644 app/authorization/server/functions/getUsersInRole.js create mode 100644 app/authorization/server/functions/hasPermission.js create mode 100644 app/authorization/server/functions/hasRole.js create mode 100644 app/authorization/server/functions/removeUserFromRoles.js create mode 100644 app/authorization/server/index.js create mode 100644 app/authorization/server/methods/addPermissionToRole.js create mode 100644 app/authorization/server/methods/addUserToRole.js create mode 100644 app/authorization/server/methods/deleteRole.js create mode 100644 app/authorization/server/methods/removeRoleFromPermission.js create mode 100644 app/authorization/server/methods/removeUserFromRole.js create mode 100644 app/authorization/server/methods/saveRole.js create mode 100644 app/authorization/server/publications/permissions.js create mode 100644 app/authorization/server/publications/permissions/emitter.js create mode 100644 app/authorization/server/publications/permissions/index.js create mode 100644 app/authorization/server/publications/roles.js create mode 100644 app/authorization/server/publications/usersInRole.js create mode 100644 app/authorization/server/startup.js create mode 100644 app/autolinker/client/client.js create mode 100644 app/autolinker/client/index.js create mode 100644 app/autolinker/server/index.js create mode 100644 app/autolinker/server/settings.js rename {packages/rocketchat-autotranslate => app/autotranslate}/README.md (100%) create mode 100644 app/autotranslate/client/index.js create mode 100644 app/autotranslate/client/lib/actionButton.js create mode 100644 app/autotranslate/client/lib/autotranslate.js create mode 100644 app/autotranslate/client/lib/tabBar.js rename {packages/rocketchat-autotranslate => app/autotranslate}/client/stylesheets/autotranslate.css (100%) rename {packages/rocketchat-autotranslate => app/autotranslate}/client/views/autoTranslateFlexTab.html (100%) rename {packages/rocketchat-autotranslate => app/autotranslate}/client/views/autoTranslateFlexTab.js (87%) create mode 100644 app/autotranslate/server/autotranslate.js create mode 100644 app/autotranslate/server/index.js create mode 100644 app/autotranslate/server/methods/getSupportedLanguages.js create mode 100644 app/autotranslate/server/methods/saveSettings.js create mode 100644 app/autotranslate/server/methods/translateMessage.js create mode 100644 app/autotranslate/server/permissions.js create mode 100644 app/autotranslate/server/settings.js create mode 100644 app/bigbluebutton/index.js rename {packages/rocketchat-bigbluebutton => app/bigbluebutton}/server/bigbluebutton-api.js (100%) create mode 100644 app/blockstack/client/index.js create mode 100644 app/blockstack/client/routes.js rename packages/rocketchat-blockstack/server/main.js => app/blockstack/server/index.js (100%) create mode 100644 app/blockstack/server/logger.js create mode 100644 app/blockstack/server/loginHandler.js create mode 100644 app/blockstack/server/routes.js create mode 100644 app/blockstack/server/settings.js rename {packages/rocketchat-blockstack => app/blockstack}/server/tokenHandler.js (100%) rename {packages/rocketchat-blockstack => app/blockstack}/server/userHandler.js (95%) rename {packages/rocketchat-bot-helpers => app/bot-helpers}/README.md (100%) create mode 100644 app/bot-helpers/index.js create mode 100644 app/bot-helpers/server/index.js create mode 100644 app/bot-helpers/server/settings.js create mode 100644 app/callbacks/client/index.js create mode 100644 app/callbacks/index.js create mode 100644 app/callbacks/lib/callbacks.js create mode 100644 app/callbacks/server/index.js create mode 100644 app/cas/client/cas_client.js create mode 100644 app/cas/client/index.js create mode 100644 app/cas/server/cas_rocketchat.js create mode 100644 app/cas/server/cas_server.js create mode 100644 app/cas/server/index.js create mode 100644 app/channel-settings-mail-messages/client/index.js create mode 100644 app/channel-settings-mail-messages/client/lib/startup.js rename {packages/rocketchat-channel-settings-mail-messages => app/channel-settings-mail-messages}/client/resetSelection.js (89%) rename {packages/rocketchat-channel-settings-mail-messages => app/channel-settings-mail-messages}/client/views/mailMessagesInstructions.html (90%) rename {packages/rocketchat-channel-settings-mail-messages => app/channel-settings-mail-messages}/client/views/mailMessagesInstructions.js (86%) create mode 100644 app/channel-settings-mail-messages/server/index.js create mode 100644 app/channel-settings-mail-messages/server/lib/startup.js create mode 100644 app/channel-settings-mail-messages/server/methods/mailMessages.js create mode 100644 app/channel-settings/client/index.js rename {packages/rocketchat-channel-settings => app/channel-settings}/client/lib/ChannelSettings.js (88%) create mode 100644 app/channel-settings/client/startup/messageTypes.js create mode 100644 app/channel-settings/client/startup/tabBar.js create mode 100644 app/channel-settings/client/startup/trackSettingsChange.js rename {packages/rocketchat-channel-settings => app/channel-settings}/client/stylesheets/channel-settings.css (98%) rename {packages/rocketchat-channel-settings => app/channel-settings}/client/views/channelSettings.html (95%) create mode 100644 app/channel-settings/client/views/channelSettings.js create mode 100644 app/channel-settings/index.js create mode 100644 app/channel-settings/server/functions/saveReactWhenReadOnly.js create mode 100644 app/channel-settings/server/functions/saveRoomAnnouncement.js create mode 100644 app/channel-settings/server/functions/saveRoomCustomFields.js create mode 100644 app/channel-settings/server/functions/saveRoomDescription.js create mode 100644 app/channel-settings/server/functions/saveRoomName.js create mode 100644 app/channel-settings/server/functions/saveRoomReadOnly.js create mode 100644 app/channel-settings/server/functions/saveRoomSystemMessages.js create mode 100644 app/channel-settings/server/functions/saveRoomTokens.js create mode 100644 app/channel-settings/server/functions/saveRoomTopic.js create mode 100644 app/channel-settings/server/functions/saveRoomType.js create mode 100644 app/channel-settings/server/functions/saveStreamingOptions.js create mode 100644 app/channel-settings/server/index.js create mode 100644 app/channel-settings/server/methods/saveRoomSettings.js create mode 100644 app/channel-settings/server/startup.js create mode 100644 app/chatpal-search/client/index.js create mode 100644 app/chatpal-search/client/route.js rename {packages => app}/chatpal-search/client/style.css (100%) rename {packages => app}/chatpal-search/client/template/admin.html (100%) create mode 100644 app/chatpal-search/client/template/admin.js create mode 100644 app/chatpal-search/client/template/result.html create mode 100644 app/chatpal-search/client/template/result.js rename {packages => app}/chatpal-search/client/template/suggestion.html (100%) rename {packages => app}/chatpal-search/client/template/suggestion.js (80%) create mode 100644 app/chatpal-search/server/asset/config.js create mode 100644 app/chatpal-search/server/index.js create mode 100644 app/chatpal-search/server/provider/index.js create mode 100644 app/chatpal-search/server/provider/provider.js create mode 100644 app/chatpal-search/server/utils/logger.js create mode 100644 app/chatpal-search/server/utils/utils.js create mode 100644 app/cloud/client/admin/callback.html create mode 100644 app/cloud/client/admin/callback.js create mode 100644 app/cloud/client/admin/cloud.html create mode 100644 app/cloud/client/admin/cloud.js create mode 100644 app/cloud/client/index.js create mode 100644 app/cloud/server/functions/checkUserHasCloudLogin.js create mode 100644 app/cloud/server/functions/connectWorkspace.js create mode 100644 app/cloud/server/functions/disconnectWorkspace.js create mode 100644 app/cloud/server/functions/finishOAuthAuthorization.js create mode 100644 app/cloud/server/functions/getOAuthAuthorizationUrl.js create mode 100644 app/cloud/server/functions/getRedirectUri.js create mode 100644 app/cloud/server/functions/getUserCloudAccessToken.js create mode 100644 app/cloud/server/functions/getWorkspaceAccessToken.js create mode 100644 app/cloud/server/functions/getWorkspaceKey.js create mode 100644 app/cloud/server/functions/getWorkspaceLicense.js create mode 100644 app/cloud/server/functions/retrieveRegistrationStatus.js create mode 100644 app/cloud/server/functions/startRegisterWorkspace.js create mode 100644 app/cloud/server/functions/syncWorkspace.js create mode 100644 app/cloud/server/functions/unregisterWorkspace.js create mode 100644 app/cloud/server/functions/userLoggedOut.js create mode 100644 app/cloud/server/functions/userLogout.js create mode 100644 app/cloud/server/index.js create mode 100644 app/cloud/server/methods.js create mode 100644 app/cloud/server/oauthScopes.js create mode 100644 app/colors/client/client.js create mode 100644 app/colors/client/index.js rename {packages/rocketchat-colors => app/colors}/client/style.css (100%) create mode 100644 app/colors/server/index.js create mode 100644 app/colors/server/settings.js create mode 100644 app/cors/client/index.js create mode 100644 app/cors/lib/common.js create mode 100644 app/cors/server/cors.js create mode 100644 app/cors/server/index.js create mode 100644 app/crowd/client/index.js create mode 100644 app/crowd/client/loginHelper.js create mode 100644 app/crowd/server/crowd.js create mode 100644 app/crowd/server/index.js create mode 100644 app/crowd/server/settings.js rename {packages/autoupdate => app/custom-oauth}/.gitignore (100%) rename {packages/rocketchat-custom-oauth => app/custom-oauth}/README.md (100%) rename {packages/rocketchat-custom-oauth => app/custom-oauth}/client/custom_oauth_client.js (85%) create mode 100644 app/custom-oauth/index.js create mode 100644 app/custom-oauth/server/custom_oauth_server.js create mode 100644 app/custom-oauth/server/oauth_helpers.js rename {packages/rocketchat-custom-sounds => app/custom-sounds}/assets/stylesheets/customSoundsAdmin.css (100%) rename {packages/rocketchat-custom-sounds => app/custom-sounds}/client/admin/adminSoundEdit.html (100%) rename {packages/rocketchat-custom-sounds => app/custom-sounds}/client/admin/adminSoundInfo.html (100%) create mode 100644 app/custom-sounds/client/admin/adminSounds.html create mode 100644 app/custom-sounds/client/admin/adminSounds.js create mode 100644 app/custom-sounds/client/admin/route.js rename {packages/rocketchat-custom-sounds => app/custom-sounds}/client/admin/soundEdit.html (100%) rename {packages/rocketchat-custom-sounds => app/custom-sounds}/client/admin/soundEdit.js (93%) rename {packages/rocketchat-custom-sounds => app/custom-sounds}/client/admin/soundInfo.html (100%) rename {packages/rocketchat-custom-sounds => app/custom-sounds}/client/admin/soundInfo.js (91%) create mode 100644 app/custom-sounds/client/admin/startup.js create mode 100644 app/custom-sounds/client/index.js create mode 100644 app/custom-sounds/client/lib/CustomSounds.js create mode 100644 app/custom-sounds/client/notifications/deleteCustomSound.js create mode 100644 app/custom-sounds/client/notifications/updateCustomSound.js create mode 100644 app/custom-sounds/server/index.js create mode 100644 app/custom-sounds/server/methods/deleteCustomSound.js create mode 100644 app/custom-sounds/server/methods/insertOrUpdateSound.js create mode 100644 app/custom-sounds/server/methods/listCustomSounds.js create mode 100644 app/custom-sounds/server/methods/uploadCustomSound.js create mode 100644 app/custom-sounds/server/publications/customSounds.js create mode 100644 app/custom-sounds/server/startup/custom-sounds.js create mode 100644 app/custom-sounds/server/startup/permissions.js create mode 100644 app/custom-sounds/server/startup/settings.js create mode 100644 app/discussion/client/createDiscussionMessageAction.js create mode 100644 app/discussion/client/discussionFromMessageBox.js create mode 100644 app/discussion/client/index.js create mode 100644 app/discussion/client/lib/discussionsOfRoom.js create mode 100644 app/discussion/client/lib/messageTypes/discussionMessage.js create mode 100644 app/discussion/client/public/stylesheets/discussion.css create mode 100644 app/discussion/client/tabBar.js create mode 100644 app/discussion/client/views/DiscussionList.html create mode 100644 app/discussion/client/views/DiscussionList.js create mode 100644 app/discussion/client/views/DiscussionTabbar.html create mode 100644 app/discussion/client/views/DiscussionTabbar.js create mode 100644 app/discussion/client/views/creationDialog/CreateDiscussion.html create mode 100755 app/discussion/client/views/creationDialog/CreateDiscussion.js create mode 100644 app/discussion/lib/discussionRoomType.js create mode 100644 app/discussion/server/authorization.js create mode 100644 app/discussion/server/config.js create mode 100644 app/discussion/server/hooks/joinDiscussionOnMessage.js create mode 100644 app/discussion/server/hooks/propagateDiscussionMetadata.js create mode 100644 app/discussion/server/index.js create mode 100644 app/discussion/server/methods/createDiscussion.js create mode 100644 app/discussion/server/permissions.js create mode 100644 app/discussion/server/publications/discussionParentAutocomplete.js create mode 100644 app/discussion/server/publications/discussionsOfRoom.js create mode 100644 app/dolphin/client/index.js rename {packages/rocketchat-dolphin => app/dolphin/client}/login-button.css (100%) create mode 100644 app/dolphin/lib/common.js create mode 100644 app/dolphin/server/index.js create mode 100644 app/dolphin/server/startup.js rename {packages/rocketchat-drupal => app/drupal}/README.md (100%) create mode 100644 app/drupal/client/index.js rename {packages/rocketchat-drupal => app/drupal/client}/login-button.css (100%) create mode 100644 app/drupal/lib/common.js create mode 100644 app/drupal/server/index.js create mode 100644 app/drupal/server/startup.js create mode 100644 app/e2e/client/accountEncryption.html create mode 100644 app/e2e/client/accountEncryption.js create mode 100644 app/e2e/client/events.js create mode 100644 app/e2e/client/helper.js create mode 100644 app/e2e/client/index.js create mode 100644 app/e2e/client/rocketchat.e2e.js rename {packages/rocketchat-e2e => app/e2e}/client/rocketchat.e2e.room.js (91%) rename {packages/rocketchat-e2e => app/e2e}/client/stylesheets/e2e.less (100%) create mode 100644 app/e2e/client/tabbar.js create mode 100644 app/e2e/server/index.js create mode 100644 app/e2e/server/methods/fetchMyKeys.js create mode 100644 app/e2e/server/methods/getUsersOfRoomWithoutKey.js create mode 100644 app/e2e/server/methods/requestSubscriptionKeys.js create mode 100644 app/e2e/server/methods/resetUserE2EKey.js rename {packages/rocketchat-e2e => app/e2e}/server/methods/setRoomKeyID.js (83%) create mode 100644 app/e2e/server/methods/setUserPublicAndPivateKeys.js create mode 100644 app/e2e/server/methods/updateGroupKey.js create mode 100644 app/e2e/server/settings.js rename {packages/rocketchat-emoji-custom => app/emoji-custom}/assets/stylesheets/emojiCustomAdmin.css (100%) create mode 100644 app/emoji-custom/client/admin/adminEmoji.html create mode 100644 app/emoji-custom/client/admin/adminEmoji.js rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/adminEmojiEdit.html (100%) rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/adminEmojiInfo.html (100%) rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/emojiEdit.html (100%) rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/emojiEdit.js (94%) rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/emojiInfo.html (100%) rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/emojiInfo.js (84%) rename {packages/rocketchat-emoji-custom => app/emoji-custom/client}/admin/emojiPreview.html (100%) create mode 100644 app/emoji-custom/client/admin/route.js create mode 100644 app/emoji-custom/client/admin/startup.js create mode 100644 app/emoji-custom/client/index.js create mode 100644 app/emoji-custom/client/lib/emojiCustom.js create mode 100644 app/emoji-custom/client/lib/function-isSet.js create mode 100644 app/emoji-custom/client/notifications/deleteEmojiCustom.js create mode 100644 app/emoji-custom/client/notifications/updateEmojiCustom.js create mode 100644 app/emoji-custom/server/index.js create mode 100644 app/emoji-custom/server/methods/deleteEmojiCustom.js create mode 100644 app/emoji-custom/server/methods/insertOrUpdateEmoji.js create mode 100644 app/emoji-custom/server/methods/listEmojiCustom.js create mode 100644 app/emoji-custom/server/methods/uploadEmojiCustom.js create mode 100644 app/emoji-custom/server/publications/fullEmojiData.js create mode 100644 app/emoji-custom/server/startup/emoji-custom.js create mode 100644 app/emoji-custom/server/startup/settings.js rename {packages/rocketchat-emoji-emojione => app/emoji-emojione}/.gitignore (100%) create mode 100644 app/emoji-emojione/README.md create mode 100644 app/emoji-emojione/client/activity-sprites.css create mode 100644 app/emoji-emojione/client/emojione-sprites.css create mode 100644 app/emoji-emojione/client/flags-sprites.css create mode 100644 app/emoji-emojione/client/food-sprites.css create mode 100644 app/emoji-emojione/client/index.js create mode 100644 app/emoji-emojione/client/modifier-sprites.css create mode 100644 app/emoji-emojione/client/nature-sprites.css create mode 100644 app/emoji-emojione/client/objects-sprites.css create mode 100644 app/emoji-emojione/client/people-sprites.css create mode 100644 app/emoji-emojione/client/regional-sprites.css create mode 100644 app/emoji-emojione/client/symbols-sprites.css create mode 100644 app/emoji-emojione/client/travel-sprites.css create mode 100644 app/emoji-emojione/lib/emojiPicker.js create mode 100644 app/emoji-emojione/lib/emojione.tpl create mode 100644 app/emoji-emojione/lib/emojioneRender.js create mode 100644 app/emoji-emojione/lib/generateEmojiIndex.mjs create mode 100644 app/emoji-emojione/lib/rocketchat.js create mode 100644 app/emoji-emojione/server/callbacks.js create mode 100644 app/emoji-emojione/server/index.js create mode 100644 app/emoji/client/emojiParser.js create mode 100644 app/emoji/client/emojiPicker.html create mode 100644 app/emoji/client/emojiPicker.js create mode 100644 app/emoji/client/function-isSet.js create mode 100644 app/emoji/client/index.js create mode 100644 app/emoji/client/lib/EmojiPicker.js create mode 100644 app/emoji/client/lib/emojiRenderer.js create mode 100644 app/emoji/index.js create mode 100644 app/emoji/lib/rocketchat.js create mode 100644 app/emoji/server/index.js create mode 100644 app/error-handler/index.js create mode 100644 app/error-handler/server/index.js create mode 100644 app/error-handler/server/lib/RocketChat.ErrorHandler.js create mode 100644 app/error-handler/server/startup/settings.js rename {packages/rocketchat-favico => app/favico}/client/favico.js (98%) create mode 100644 app/favico/client/index.js create mode 100644 app/favico/index.js create mode 100644 app/federation/README.md create mode 100644 app/federation/client/admin/dashboard.css create mode 100644 app/federation/client/admin/dashboard.html create mode 100644 app/federation/client/admin/dashboard.js create mode 100644 app/federation/client/index.js create mode 100644 app/federation/client/messageTypes.js create mode 100644 app/federation/server/PeerClient.js create mode 100644 app/federation/server/PeerDNS.js create mode 100644 app/federation/server/PeerHTTP/PeerHTTP.js create mode 100644 app/federation/server/PeerHTTP/index.js create mode 100644 app/federation/server/PeerHTTP/utils.js create mode 100644 app/federation/server/PeerPinger.js create mode 100644 app/federation/server/PeerServer/PeerServer.js create mode 100644 app/federation/server/PeerServer/index.js create mode 100644 app/federation/server/PeerServer/routes/events.js create mode 100644 app/federation/server/PeerServer/routes/uploads.js create mode 100644 app/federation/server/PeerServer/routes/users.js create mode 100644 app/federation/server/config.js create mode 100644 app/federation/server/federatedResources/FederatedMessage.js create mode 100644 app/federation/server/federatedResources/FederatedResource.js create mode 100644 app/federation/server/federatedResources/FederatedRoom.js create mode 100644 app/federation/server/federatedResources/FederatedUser.js create mode 100644 app/federation/server/federatedResources/index.js create mode 100644 app/federation/server/federation-settings.js create mode 100644 app/federation/server/index.js create mode 100644 app/federation/server/logger.js create mode 100644 app/federation/server/methods/addUser.js create mode 100644 app/federation/server/methods/dashboard.js create mode 100644 app/federation/server/methods/ping.js create mode 100644 app/federation/server/methods/searchUsers.js create mode 100644 app/federation/server/settingsUpdater.js create mode 100644 app/file-upload/client/index.js create mode 100644 app/file-upload/client/lib/fileUploadHandler.js create mode 100644 app/file-upload/index.js create mode 100644 app/file-upload/lib/FileUploadBase.js create mode 100644 app/file-upload/server/config/AmazonS3.js rename {packages/rocketchat-file-upload => app/file-upload}/server/config/FileSystem.js (89%) rename {packages/rocketchat-file-upload => app/file-upload}/server/config/GoogleStorage.js (78%) rename {packages/rocketchat-file-upload => app/file-upload}/server/config/GridFS.js (95%) create mode 100644 app/file-upload/server/config/Webdav.js create mode 100644 app/file-upload/server/config/_configUploadStorage.js create mode 100644 app/file-upload/server/index.js create mode 100644 app/file-upload/server/lib/FileUpload.js create mode 100644 app/file-upload/server/lib/proxy.js create mode 100644 app/file-upload/server/lib/requests.js create mode 100644 app/file-upload/server/methods/getS3FileUrl.js create mode 100644 app/file-upload/server/methods/sendFileMessage.js create mode 100644 app/file-upload/server/startup/settings.js create mode 100644 app/file-upload/ufs/AmazonS3/server.js create mode 100644 app/file-upload/ufs/GoogleStorage/server.js create mode 100644 app/file-upload/ufs/Webdav/server.js create mode 100644 app/file/index.js rename {packages/rocketchat-file => app/file/server}/file.server.js (95%) create mode 100644 app/file/server/index.js rename {packages/rocketchat-github-enterprise => app/github-enterprise/client}/github-enterprise-login-button.css (100%) create mode 100644 app/github-enterprise/client/index.js create mode 100644 app/github-enterprise/lib/common.js create mode 100644 app/github-enterprise/server/index.js create mode 100644 app/github-enterprise/server/startup.js rename {packages/rocketchat-gitlab => app/gitlab/client}/gitlab-login-button.css (100%) create mode 100644 app/gitlab/client/index.js create mode 100644 app/gitlab/lib/common.js create mode 100644 app/gitlab/server/index.js create mode 100644 app/gitlab/server/startup.js rename {packages/rocketchat-google-vision => app/google-vision}/README.md (100%) create mode 100644 app/google-vision/client/googlevision.js create mode 100644 app/google-vision/client/index.js create mode 100644 app/google-vision/server/googlevision.js create mode 100644 app/google-vision/server/index.js create mode 100644 app/google-vision/server/settings.js rename {packages/rocketchat-grant-facebook => app/grant-facebook}/README.md (100%) create mode 100644 app/grant-facebook/index.js create mode 100644 app/grant-facebook/server/index.js rename {packages/rocketchat-grant-github => app/grant-github}/README.md (100%) create mode 100644 app/grant-github/index.js create mode 100644 app/grant-github/server/index.js rename {packages/rocketchat-grant-google => app/grant-google}/README.md (100%) create mode 100644 app/grant-google/index.js create mode 100644 app/grant-google/server/index.js rename {packages/rocketchat-grant => app/grant}/README.md (100%) create mode 100644 app/grant/index.js rename {packages/rocketchat-grant => app/grant}/server/authenticate.js (75%) create mode 100644 app/grant/server/error.js rename {packages/rocketchat-grant => app/grant}/server/grant.js (92%) create mode 100644 app/grant/server/index.js create mode 100644 app/grant/server/providers.js rename {packages/rocketchat-grant => app/grant}/server/redirect.js (100%) rename {packages/rocketchat-grant => app/grant}/server/routes.js (100%) create mode 100644 app/grant/server/settings.js rename {packages/rocketchat-grant => app/grant}/server/storage.js (100%) rename {packages/rocketchat-graphql => app/graphql}/README.md (100%) create mode 100644 app/graphql/index.js create mode 100644 app/graphql/server/api.js rename {packages/rocketchat-graphql => app/graphql}/server/helpers/authenticated.js (79%) rename {packages/rocketchat-graphql => app/graphql}/server/helpers/dateToFloat.js (100%) create mode 100644 app/graphql/server/index.js rename {packages/rocketchat-graphql => app/graphql}/server/mocks/accounts/graphql-api.js (83%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/accounts/OauthProvider-type.js (100%) create mode 100644 app/graphql/server/resolvers/accounts/index.js rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/accounts/oauthProviders.js (87%) create mode 100644 app/graphql/server/resolvers/channels/Channel-type.js rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/ChannelFilter-input.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/ChannelNameAndDirect-input.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/ChannelSort-enum.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/Privacy-enum.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/channelByName.js (78%) create mode 100644 app/graphql/server/resolvers/channels/channels.js create mode 100644 app/graphql/server/resolvers/channels/channelsByUser.js create mode 100644 app/graphql/server/resolvers/channels/createChannel.js create mode 100644 app/graphql/server/resolvers/channels/deleteChannel.js rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/directChannel.js (88%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/hideChannel.js (80%) create mode 100644 app/graphql/server/resolvers/channels/index.js rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/leaveChannel.js (85%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/channels/settings.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/Message-type.js (82%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/MessageIdentifier-input.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/MessagesWithCursor-type.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/Reaction-type.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/addReactionToMessage.js (80%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/chatMessageAdded.js (80%) create mode 100644 app/graphql/server/resolvers/messages/deleteMessage.js rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/editMessage.js (81%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/messages/index.js (100%) create mode 100644 app/graphql/server/resolvers/messages/messages.js create mode 100644 app/graphql/server/resolvers/messages/sendMessage.js create mode 100644 app/graphql/server/resolvers/users/User-type.js rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/users/UserStatus-enum.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/resolvers/users/index.js (100%) create mode 100644 app/graphql/server/resolvers/users/setStatus.js rename {packages/rocketchat-graphql => app/graphql}/server/schema.js (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/accounts/LoginResult-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/accounts/OauthProvider-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/accounts/oauthProviders.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/Channel-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/ChannelFilter-input.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/ChannelNameAndDirect-input.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/ChannelSort-enum.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/Privacy-enum.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/channelByName.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/channels.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/channelsByUser.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/createChannel.graphqls (100%) create mode 100644 app/graphql/server/schemas/channels/deleteChannel.graphqls rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/directChannel.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/hideChannel.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/channels/leaveChannel.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/Message-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/MessageIdentifier-input.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/MessagesWithCursor-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/Reaction-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/addReactionToMessage.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/chatMessageAdded.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/deleteMessage.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/editMessage.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/messages.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/messages/sendMessage.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/users/User-type.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/users/UserStatus-enum.graphqls (100%) rename {packages/rocketchat-graphql => app/graphql}/server/schemas/users/setStatus.graphqls (100%) create mode 100644 app/graphql/server/settings.js rename {packages/rocketchat-graphql => app/graphql}/server/subscriptions.js (100%) create mode 100644 app/highlight-words/client/client.js create mode 100644 app/highlight-words/client/helper.js create mode 100644 app/highlight-words/client/index.js create mode 100644 app/highlight-words/index.js create mode 100644 app/highlight-words/tests/helper.tests.js create mode 100644 app/iframe-login/client/iframe_client.js create mode 100644 app/iframe-login/client/index.js rename {packages/rocketchat-iframe-login => app/iframe-login/server}/iframe_rocketchat.js (75%) create mode 100644 app/iframe-login/server/iframe_server.js create mode 100644 app/iframe-login/server/index.js create mode 100644 app/importer-csv/client/adder.js create mode 100644 app/importer-csv/client/index.js create mode 100644 app/importer-csv/lib/info.js create mode 100644 app/importer-csv/server/adder.js create mode 100644 app/importer-csv/server/importer.js create mode 100644 app/importer-csv/server/index.js create mode 100644 app/importer-hipchat-enterprise/client/adder.js create mode 100644 app/importer-hipchat-enterprise/client/index.js create mode 100644 app/importer-hipchat-enterprise/lib/info.js create mode 100644 app/importer-hipchat-enterprise/server/adder.js create mode 100644 app/importer-hipchat-enterprise/server/importer.js create mode 100644 app/importer-hipchat-enterprise/server/index.js create mode 100644 app/importer-hipchat/client/adder.js create mode 100644 app/importer-hipchat/client/index.js create mode 100644 app/importer-hipchat/lib/info.js create mode 100644 app/importer-hipchat/server/adder.js create mode 100644 app/importer-hipchat/server/importer.js create mode 100644 app/importer-hipchat/server/index.js create mode 100644 app/importer-slack-users/client/adder.js create mode 100644 app/importer-slack-users/client/index.js create mode 100644 app/importer-slack-users/lib/info.js create mode 100644 app/importer-slack-users/server/adder.js create mode 100644 app/importer-slack-users/server/importer.js create mode 100644 app/importer-slack-users/server/index.js create mode 100644 app/importer-slack/client/adder.js create mode 100644 app/importer-slack/client/index.js create mode 100644 app/importer-slack/lib/info.js create mode 100644 app/importer-slack/server/adder.js create mode 100644 app/importer-slack/server/importer.js create mode 100644 app/importer-slack/server/index.js rename {packages/rocketchat-importer => app/importer}/client/ImporterWebsocketReceiver.js (81%) create mode 100644 app/importer/client/admin/adminImport.html create mode 100644 app/importer/client/admin/adminImport.js create mode 100644 app/importer/client/admin/adminImportHistory.html create mode 100644 app/importer/client/admin/adminImportHistory.js create mode 100644 app/importer/client/admin/adminImportPrepare.html create mode 100644 app/importer/client/admin/adminImportPrepare.js create mode 100644 app/importer/client/admin/adminImportProgress.html rename {packages/rocketchat-importer => app/importer}/client/admin/adminImportProgress.js (89%) create mode 100644 app/importer/client/index.js rename {packages/rocketchat-importer => app/importer}/lib/ImporterInfo.js (100%) rename {packages/rocketchat-importer => app/importer}/lib/ImporterProgressStep.js (85%) rename {packages/rocketchat-importer => app/importer}/lib/Importers.js (100%) create mode 100644 app/importer/server/classes/ImporterBase.js rename {packages/rocketchat-importer => app/importer}/server/classes/ImporterProgress.js (100%) rename {packages/rocketchat-importer => app/importer}/server/classes/ImporterSelection.js (100%) rename {packages/rocketchat-importer => app/importer}/server/classes/ImporterSelectionChannel.js (87%) rename {packages/rocketchat-importer => app/importer}/server/classes/ImporterSelectionUser.js (80%) rename {packages/rocketchat-importer => app/importer}/server/classes/ImporterWebsocket.js (92%) create mode 100644 app/importer/server/index.js create mode 100644 app/importer/server/methods/downloadPublicImportFile.js create mode 100644 app/importer/server/methods/getImportFileData.js rename {packages/rocketchat-importer => app/importer}/server/methods/getImportProgress.js (76%) create mode 100644 app/importer/server/methods/getLatestImportOperations.js rename {packages/rocketchat-importer => app/importer}/server/methods/getSelectionData.js (82%) rename {packages/rocketchat-importer => app/importer}/server/methods/prepareImport.js (76%) create mode 100644 app/importer/server/methods/restartImport.js rename {packages/rocketchat-importer => app/importer}/server/methods/setupImporter.js (80%) rename {packages/rocketchat-importer => app/importer}/server/methods/startImport.js (88%) create mode 100644 app/importer/server/methods/uploadImportFile.js create mode 100644 app/importer/server/models/Imports.js create mode 100644 app/importer/server/models/RawImports.js rename {packages/rocketchat-importer => app/importer}/server/startup/setImportsToInvalid.js (93%) create mode 100644 app/importer/server/startup/store.js create mode 100644 app/integrations/client/collections.js create mode 100644 app/integrations/client/index.js create mode 100644 app/integrations/client/route.js create mode 100644 app/integrations/client/startup.js rename {packages/rocketchat-integrations => app/integrations}/client/stylesheets/integrations.css (96%) rename {packages/rocketchat-integrations => app/integrations}/client/views/additional/zapier.html (100%) rename {packages/rocketchat-integrations => app/integrations}/client/views/integrations.html (98%) create mode 100644 app/integrations/client/views/integrations.js rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsIncoming.html (95%) rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsIncoming.js (82%) rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsNew.html (96%) create mode 100644 app/integrations/client/views/integrationsNew.js rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsOutgoing.html (97%) rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsOutgoing.js (84%) rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsOutgoingHistory.html (99%) rename {packages/rocketchat-integrations => app/integrations}/client/views/integrationsOutgoingHistory.js (76%) create mode 100644 app/integrations/client/views/messageExample.js create mode 100644 app/integrations/lib/rocketchat.js create mode 100644 app/integrations/server/api/api.js create mode 100644 app/integrations/server/index.js rename {packages/rocketchat-integrations => app/integrations}/server/lib/triggerHandler.js (92%) rename {packages/rocketchat-integrations => app/integrations}/server/lib/validation.js (81%) create mode 100644 app/integrations/server/logger.js create mode 100644 app/integrations/server/methods/clearIntegrationHistory.js create mode 100644 app/integrations/server/methods/incoming/addIncomingIntegration.js create mode 100644 app/integrations/server/methods/incoming/deleteIncomingIntegration.js create mode 100644 app/integrations/server/methods/incoming/updateIncomingIntegration.js create mode 100644 app/integrations/server/methods/outgoing/addOutgoingIntegration.js create mode 100644 app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js create mode 100644 app/integrations/server/methods/outgoing/replayOutgoingIntegration.js create mode 100644 app/integrations/server/methods/outgoing/updateOutgoingIntegration.js create mode 100644 app/integrations/server/publications/integrationHistory.js create mode 100644 app/integrations/server/publications/integrations.js create mode 100644 app/integrations/server/triggers.js rename {packages/rocketchat-irc => app/irc}/README.md (100%) create mode 100644 app/irc/index.js create mode 100644 app/irc/server/index.js create mode 100644 app/irc/server/irc-bridge/index.js rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/index.js (100%) create mode 100644 app/irc/server/irc-bridge/localHandlers/onCreateRoom.js rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/onCreateUser.js (80%) rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/onJoinRoom.js (100%) rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/onLeaveRoom.js (100%) rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/onLogin.js (80%) rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/onLogout.js (100%) rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/localHandlers/onSaveMessage.js (75%) create mode 100644 app/irc/server/irc-bridge/peerHandlers/disconnected.js rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/peerHandlers/index.js (100%) create mode 100644 app/irc/server/irc-bridge/peerHandlers/joinedChannel.js create mode 100644 app/irc/server/irc-bridge/peerHandlers/leftChannel.js rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/peerHandlers/nickChanged.js (77%) create mode 100644 app/irc/server/irc-bridge/peerHandlers/sentMessage.js rename {packages/rocketchat-irc => app/irc}/server/irc-bridge/peerHandlers/userRegistered.js (86%) rename {packages/rocketchat-irc => app/irc}/server/irc-settings.js (90%) create mode 100644 app/irc/server/irc.js create mode 100644 app/irc/server/methods/resetIrcConnection.js rename {packages/rocketchat-irc => app/irc}/server/servers/RFC2813/codes.js (100%) create mode 100644 app/irc/server/servers/RFC2813/index.js rename {packages/rocketchat-irc => app/irc}/server/servers/RFC2813/localCommandHandlers.js (80%) rename {packages/rocketchat-irc => app/irc}/server/servers/RFC2813/parseMessage.js (96%) rename {packages/rocketchat-irc => app/irc}/server/servers/RFC2813/peerCommandHandlers.js (100%) rename {packages/rocketchat-irc => app/irc}/server/servers/index.js (100%) create mode 100644 app/issuelinks/client/client.js create mode 100644 app/issuelinks/client/index.js create mode 100644 app/issuelinks/server/index.js create mode 100644 app/issuelinks/server/settings.js create mode 100644 app/katex/.eslintrc create mode 100644 app/katex/client/index.js rename {packages/rocketchat-katex => app/katex}/client/style.css (100%) create mode 100644 app/katex/katex.min.css create mode 100644 app/katex/lib/katex.js create mode 100644 app/katex/server/index.js create mode 100644 app/katex/server/settings.js create mode 100644 app/lazy-load/client/index.js rename {packages/rocketchat-lazy-load => app/lazy-load}/client/lazyloadImage.html (100%) create mode 100644 app/lazy-load/client/lazyloadImage.js create mode 100644 app/lazy-load/index.js create mode 100644 app/ldap/client/index.js create mode 100644 app/ldap/client/loginHelper.js rename {packages/rocketchat-ldap => app/ldap}/server/index.js (100%) rename {packages/rocketchat-ldap => app/ldap}/server/ldap.js (83%) create mode 100644 app/ldap/server/loginHandler.js create mode 100644 app/ldap/server/settings.js rename {packages/rocketchat-ldap => app/ldap}/server/sync.js (76%) create mode 100644 app/ldap/server/syncUsers.js rename {packages/rocketchat-ldap => app/ldap}/server/testConnection.js (77%) create mode 100644 app/lib/README.md create mode 100644 app/lib/client/CustomTranslations.js create mode 100644 app/lib/client/OAuthProxy.js create mode 100644 app/lib/client/UserDeleted.js create mode 100644 app/lib/client/defaultTabBars.js create mode 100644 app/lib/client/index.js rename {packages/rocketchat-lib => app/lib}/client/lib/LoginPresence.js (85%) rename {packages/rocketchat-lib => app/lib}/client/lib/RocketChatAnnouncement.js (89%) create mode 100644 app/lib/client/lib/formatDate.js create mode 100644 app/lib/client/lib/index.js create mode 100644 app/lib/client/lib/settings.js create mode 100644 app/lib/client/lib/startup/commands.js rename {packages/rocketchat-lib => app/lib}/client/lib/userRoles.js (79%) create mode 100644 app/lib/client/methods/sendMessage.js rename {packages/rocketchat-lib => app/lib}/client/views/customFieldsForm.html (100%) rename {packages/rocketchat-lib => app/lib}/client/views/customFieldsForm.js (78%) create mode 100644 app/lib/index.js create mode 100644 app/lib/lib/MessageTypes.js create mode 100644 app/lib/lib/roomTypes/conversation.js create mode 100644 app/lib/lib/roomTypes/direct.js create mode 100644 app/lib/lib/roomTypes/favorite.js rename {packages/rocketchat-lib => app/lib}/lib/roomTypes/index.js (100%) create mode 100644 app/lib/lib/roomTypes/private.js create mode 100644 app/lib/lib/roomTypes/public.js create mode 100644 app/lib/lib/roomTypes/unread.js create mode 100644 app/lib/lib/startup/settingsOnLoadSiteUrl.js create mode 100644 app/lib/server/functions/addUserToDefaultChannels.js create mode 100644 app/lib/server/functions/addUserToRoom.js create mode 100644 app/lib/server/functions/archiveRoom.js create mode 100644 app/lib/server/functions/attachMessage.js create mode 100644 app/lib/server/functions/checkEmailAvailability.js create mode 100644 app/lib/server/functions/checkUsernameAvailability.js create mode 100644 app/lib/server/functions/cleanRoomHistory.js create mode 100644 app/lib/server/functions/createRoom.js create mode 100644 app/lib/server/functions/deleteMessage.js create mode 100644 app/lib/server/functions/deleteRoom.js create mode 100644 app/lib/server/functions/deleteUser.js create mode 100644 app/lib/server/functions/getAvatarSuggestionForUser.js create mode 100644 app/lib/server/functions/getFullUserData.js create mode 100644 app/lib/server/functions/getRoomByNameOrIdWithOptionToJoin.js create mode 100644 app/lib/server/functions/getStatusText.js create mode 100644 app/lib/server/functions/getUsernameSuggestion.js create mode 100644 app/lib/server/functions/index.js create mode 100644 app/lib/server/functions/insertMessage.js create mode 100644 app/lib/server/functions/isTheLastMessage.js create mode 100644 app/lib/server/functions/loadMessageHistory.js create mode 100644 app/lib/server/functions/notifications/audio.js create mode 100644 app/lib/server/functions/notifications/desktop.js create mode 100644 app/lib/server/functions/notifications/email.js create mode 100644 app/lib/server/functions/notifications/index.js create mode 100644 app/lib/server/functions/notifications/mobile.js create mode 100644 app/lib/server/functions/processWebhookMessage.js create mode 100644 app/lib/server/functions/removeUserFromRoom.js create mode 100644 app/lib/server/functions/saveCustomFields.js create mode 100644 app/lib/server/functions/saveCustomFieldsWithoutValidation.js create mode 100644 app/lib/server/functions/saveUser.js create mode 100644 app/lib/server/functions/sendMessage.js create mode 100644 app/lib/server/functions/setEmail.js create mode 100644 app/lib/server/functions/setRealName.js create mode 100644 app/lib/server/functions/setStatusText.js rename {packages/rocketchat-lib => app/lib}/server/functions/setUserAvatar.js (76%) create mode 100644 app/lib/server/functions/setUsername.js create mode 100644 app/lib/server/functions/unarchiveRoom.js create mode 100644 app/lib/server/functions/updateMessage.js rename {packages/rocketchat-lib => app/lib}/server/functions/validateCustomFields.js (83%) create mode 100644 app/lib/server/index.js rename {packages/rocketchat-lib => app/lib}/server/lib/PasswordPolicyClass.js (98%) create mode 100644 app/lib/server/lib/RateLimiter.js create mode 100644 app/lib/server/lib/bugsnag.js create mode 100644 app/lib/server/lib/configLogger.js create mode 100644 app/lib/server/lib/debug.js rename {packages/rocketchat-lib => app/lib}/server/lib/defaultBlockedDomainsList.js (99%) create mode 100644 app/lib/server/lib/index.js rename {packages/rocketchat-lib => app/lib}/server/lib/interceptDirectReplyEmails.js (80%) rename {packages/rocketchat-lib => app/lib}/server/lib/loginErrorMessageOverride.js (81%) create mode 100644 app/lib/server/lib/meteorFixes.js create mode 100644 app/lib/server/lib/msgStream.js create mode 100644 app/lib/server/lib/notifyUsersOnMessage.js create mode 100644 app/lib/server/lib/passwordPolicy.js create mode 100644 app/lib/server/lib/processDirectEmail.js create mode 100644 app/lib/server/lib/sendNotificationsOnMessage.js create mode 100644 app/lib/server/lib/validateEmailDomain.js create mode 100644 app/lib/server/methods/addOAuthService.js create mode 100644 app/lib/server/methods/addUserToRoom.js create mode 100644 app/lib/server/methods/addUsersToRoom.js create mode 100644 app/lib/server/methods/archiveRoom.js create mode 100644 app/lib/server/methods/blockUser.js create mode 100644 app/lib/server/methods/checkRegistrationSecretURL.js create mode 100644 app/lib/server/methods/checkUsernameAvailability.js create mode 100644 app/lib/server/methods/cleanRoomHistory.js create mode 100644 app/lib/server/methods/createChannel.js create mode 100644 app/lib/server/methods/createPrivateGroup.js create mode 100644 app/lib/server/methods/createToken.js create mode 100644 app/lib/server/methods/deleteMessage.js create mode 100644 app/lib/server/methods/deleteUserOwnAccount.js create mode 100644 app/lib/server/methods/executeSlashCommandPreview.js create mode 100644 app/lib/server/methods/filterATAllTag.js create mode 100644 app/lib/server/methods/filterATHereTag.js create mode 100644 app/lib/server/methods/filterBadWords.js create mode 100644 app/lib/server/methods/getChannelHistory.js create mode 100644 app/lib/server/methods/getFullUserData.js create mode 100644 app/lib/server/methods/getMessages.js create mode 100644 app/lib/server/methods/getRoomJoinCode.js create mode 100644 app/lib/server/methods/getRoomRoles.js create mode 100644 app/lib/server/methods/getServerInfo.js create mode 100644 app/lib/server/methods/getSingleMessage.js create mode 100644 app/lib/server/methods/getSlashCommandPreviews.js create mode 100644 app/lib/server/methods/getUserRoles.js create mode 100644 app/lib/server/methods/getUsernameSuggestion.js create mode 100644 app/lib/server/methods/insertOrUpdateUser.js create mode 100644 app/lib/server/methods/joinDefaultChannels.js create mode 100644 app/lib/server/methods/joinRoom.js create mode 100644 app/lib/server/methods/leaveRoom.js create mode 100644 app/lib/server/methods/refreshOAuthService.js create mode 100644 app/lib/server/methods/removeOAuthService.js rename {packages/rocketchat-lib => app/lib}/server/methods/restartServer.js (79%) create mode 100644 app/lib/server/methods/robotMethods.js create mode 100644 app/lib/server/methods/saveSetting.js create mode 100644 app/lib/server/methods/saveSettings.js create mode 100644 app/lib/server/methods/sendInvitationEmail.js create mode 100644 app/lib/server/methods/sendMessage.js create mode 100644 app/lib/server/methods/sendSMTPTestEmail.js create mode 100644 app/lib/server/methods/setAdminStatus.js create mode 100644 app/lib/server/methods/setEmail.js create mode 100644 app/lib/server/methods/setRealName.js create mode 100644 app/lib/server/methods/setUsername.js create mode 100644 app/lib/server/methods/unarchiveRoom.js create mode 100644 app/lib/server/methods/unblockUser.js create mode 100644 app/lib/server/methods/updateMessage.js create mode 100644 app/lib/server/oauth/facebook.js rename {packages/rocketchat-lib => app/lib}/server/oauth/google.js (83%) create mode 100644 app/lib/server/oauth/oauth.js create mode 100644 app/lib/server/oauth/proxy.js rename {packages/rocketchat-lib => app/lib}/server/oauth/twitter.js (83%) create mode 100644 app/lib/server/publications/settings.js create mode 100644 app/lib/server/startup/email.js create mode 100644 app/lib/server/startup/oAuthServicesUpdate.js create mode 100644 app/lib/server/startup/rateLimiter.js create mode 100644 app/lib/server/startup/robots.js create mode 100644 app/lib/server/startup/settings.js create mode 100644 app/lib/server/startup/settingsOnLoadCdnPrefix.js create mode 100644 app/lib/server/startup/settingsOnLoadDirectReply.js create mode 100644 app/lib/server/startup/settingsOnLoadSMTP.js create mode 100644 app/lib/startup/defaultRoomTypes.js create mode 100644 app/lib/startup/index.js create mode 100644 app/lib/tests/server.mocks.js create mode 100644 app/lib/tests/server.tests.js create mode 100644 app/livechat/client/collections/AgentUsers.js create mode 100644 app/livechat/client/collections/LivechatCustomField.js create mode 100644 app/livechat/client/collections/LivechatDepartment.js create mode 100644 app/livechat/client/collections/LivechatDepartmentAgents.js create mode 100644 app/livechat/client/collections/LivechatIntegration.js create mode 100644 app/livechat/client/collections/LivechatMonitoring.js create mode 100644 app/livechat/client/collections/LivechatQueueUser.js create mode 100644 app/livechat/client/collections/LivechatTrigger.js create mode 100644 app/livechat/client/collections/LivechatVisitor.js create mode 100644 app/livechat/client/collections/livechatOfficeHour.js create mode 100644 app/livechat/client/index.js rename {packages/rocketchat-livechat => app/livechat}/client/lib/chartHandler.js (98%) rename {packages/rocketchat-livechat => app/livechat}/client/lib/dataHandler.js (91%) rename {packages/rocketchat-livechat => app/livechat}/client/lib/dateHandler.js (76%) create mode 100644 app/livechat/client/roomType.js create mode 100644 app/livechat/client/route.js create mode 100644 app/livechat/client/startup/notifyUnreadRooms.js rename {packages/rocketchat-livechat => app/livechat}/client/stylesheets/livechat.less (99%) create mode 100644 app/livechat/client/ui.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatAnalytics.html (95%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatAnalytics.js (94%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatAnalyticsCustomDaterange.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatAnalyticsCustomDaterange.js (85%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatAnalyticsDaterange.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatAnalyticsDaterange.js (88%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatRealTimeMonitoring.html (97%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/analytics/livechatRealTimeMonitoring.js (95%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/integrations/livechatIntegrationFacebook.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/integrations/livechatIntegrationFacebook.js (93%) create mode 100644 app/livechat/client/views/app/integrations/livechatIntegrationWebhook.html rename {packages/rocketchat-livechat => app/livechat}/client/views/app/integrations/livechatIntegrationWebhook.js (91%) create mode 100644 app/livechat/client/views/app/livechatAppearance.html create mode 100644 app/livechat/client/views/app/livechatAppearance.js create mode 100644 app/livechat/client/views/app/livechatAutocompleteUser.html create mode 100644 app/livechat/client/views/app/livechatAutocompleteUser.js create mode 100644 app/livechat/client/views/app/livechatCurrentChats.html create mode 100644 app/livechat/client/views/app/livechatCurrentChats.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatCustomFieldForm.html (85%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatCustomFieldForm.js (82%) create mode 100644 app/livechat/client/views/app/livechatCustomFields.html create mode 100644 app/livechat/client/views/app/livechatCustomFields.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatDashboard.html (100%) create mode 100644 app/livechat/client/views/app/livechatDepartmentForm.html rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatDepartmentForm.js (77%) create mode 100644 app/livechat/client/views/app/livechatDepartments.html create mode 100644 app/livechat/client/views/app/livechatDepartments.js create mode 100644 app/livechat/client/views/app/livechatInstallation.html create mode 100644 app/livechat/client/views/app/livechatInstallation.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatNotSubscribed.html (100%) create mode 100644 app/livechat/client/views/app/livechatOfficeHours.html create mode 100644 app/livechat/client/views/app/livechatOfficeHours.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatQueue.html (100%) create mode 100644 app/livechat/client/views/app/livechatQueue.js create mode 100644 app/livechat/client/views/app/livechatReadOnly.html create mode 100644 app/livechat/client/views/app/livechatReadOnly.js create mode 100644 app/livechat/client/views/app/livechatTriggers.html create mode 100644 app/livechat/client/views/app/livechatTriggers.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatTriggersForm.html (87%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/livechatTriggersForm.js (89%) create mode 100644 app/livechat/client/views/app/livechatUsers.html create mode 100644 app/livechat/client/views/app/livechatUsers.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/externalSearch.html (100%) create mode 100644 app/livechat/client/views/app/tabbar/externalSearch.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorEdit.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorEdit.js (83%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorForward.html (100%) create mode 100644 app/livechat/client/views/app/tabbar/visitorForward.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorHistory.html (100%) create mode 100644 app/livechat/client/views/app/tabbar/visitorHistory.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorInfo.html (89%) create mode 100644 app/livechat/client/views/app/tabbar/visitorInfo.js rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorNavigation.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/tabbar/visitorNavigation.js (79%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/triggers/livechatTriggerAction.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/triggers/livechatTriggerAction.js (77%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/triggers/livechatTriggerCondition.html (100%) rename {packages/rocketchat-livechat => app/livechat}/client/views/app/triggers/livechatTriggerCondition.js (85%) rename {packages/rocketchat-livechat => app/livechat}/client/views/sideNav/livechat.html (100%) create mode 100644 app/livechat/client/views/sideNav/livechat.js rename {packages/rocketchat-livechat => app/livechat}/client/views/sideNav/livechatFlex.html (100%) create mode 100644 app/livechat/client/views/sideNav/livechatFlex.js create mode 100644 app/livechat/imports/server/rest/departments.js create mode 100644 app/livechat/imports/server/rest/facebook.js create mode 100644 app/livechat/imports/server/rest/inquiries.js rename {packages/rocketchat-livechat => app/livechat}/imports/server/rest/sms.js (75%) create mode 100644 app/livechat/imports/server/rest/upload.js create mode 100644 app/livechat/imports/server/rest/users.js create mode 100644 app/livechat/lib/Assets.js create mode 100644 app/livechat/lib/LivechatExternalMessage.js create mode 100644 app/livechat/lib/LivechatInquiry.js create mode 100644 app/livechat/lib/LivechatRoomType.js create mode 100644 app/livechat/lib/messageTypes.js create mode 100644 app/livechat/server/agentStatus.js create mode 100644 app/livechat/server/api.js create mode 100644 app/livechat/server/api/lib/livechat.js rename {packages/rocketchat-livechat => app/livechat}/server/api/rest.js (100%) create mode 100644 app/livechat/server/api/v1/agent.js create mode 100644 app/livechat/server/api/v1/config.js create mode 100644 app/livechat/server/api/v1/customField.js create mode 100644 app/livechat/server/api/v1/message.js create mode 100644 app/livechat/server/api/v1/offlineMessage.js create mode 100644 app/livechat/server/api/v1/pageVisited.js create mode 100644 app/livechat/server/api/v1/room.js create mode 100644 app/livechat/server/api/v1/transcript.js create mode 100644 app/livechat/server/api/v1/videoCall.js create mode 100644 app/livechat/server/api/v1/visitor.js create mode 100644 app/livechat/server/config.js create mode 100644 app/livechat/server/hooks/RDStation.js create mode 100644 app/livechat/server/hooks/externalMessage.js create mode 100644 app/livechat/server/hooks/leadCapture.js create mode 100644 app/livechat/server/hooks/markRoomResponded.js create mode 100644 app/livechat/server/hooks/offlineMessage.js create mode 100644 app/livechat/server/hooks/saveAnalyticsData.js create mode 100644 app/livechat/server/hooks/sendToCRM.js create mode 100644 app/livechat/server/hooks/sendToFacebook.js create mode 100644 app/livechat/server/index.js rename {packages/rocketchat-livechat => app/livechat}/server/lib/Analytics.js (88%) create mode 100644 app/livechat/server/lib/Livechat.js create mode 100644 app/livechat/server/lib/OfficeClock.js create mode 100644 app/livechat/server/lib/OmniChannel.js create mode 100644 app/livechat/server/lib/QueueMethods.js create mode 100644 app/livechat/server/livechat.js create mode 100644 app/livechat/server/methods/addAgent.js create mode 100644 app/livechat/server/methods/addManager.js create mode 100644 app/livechat/server/methods/changeLivechatStatus.js create mode 100644 app/livechat/server/methods/closeByVisitor.js create mode 100644 app/livechat/server/methods/closeRoom.js create mode 100644 app/livechat/server/methods/facebook.js create mode 100644 app/livechat/server/methods/getAgentData.js create mode 100644 app/livechat/server/methods/getAgentOverviewData.js create mode 100644 app/livechat/server/methods/getAnalyticsChartData.js create mode 100644 app/livechat/server/methods/getAnalyticsOverviewData.js create mode 100644 app/livechat/server/methods/getCustomFields.js create mode 100644 app/livechat/server/methods/getFirstRoomMessage.js create mode 100644 app/livechat/server/methods/getInitialData.js create mode 100644 app/livechat/server/methods/getNextAgent.js create mode 100644 app/livechat/server/methods/loadHistory.js create mode 100644 app/livechat/server/methods/loginByToken.js create mode 100644 app/livechat/server/methods/pageVisited.js create mode 100644 app/livechat/server/methods/registerGuest.js create mode 100644 app/livechat/server/methods/removeAgent.js create mode 100644 app/livechat/server/methods/removeCustomField.js create mode 100644 app/livechat/server/methods/removeDepartment.js create mode 100644 app/livechat/server/methods/removeManager.js create mode 100644 app/livechat/server/methods/removeRoom.js create mode 100644 app/livechat/server/methods/removeTrigger.js create mode 100644 app/livechat/server/methods/returnAsInquiry.js create mode 100644 app/livechat/server/methods/saveAppearance.js create mode 100644 app/livechat/server/methods/saveCustomField.js create mode 100644 app/livechat/server/methods/saveDepartment.js create mode 100644 app/livechat/server/methods/saveInfo.js create mode 100644 app/livechat/server/methods/saveIntegration.js create mode 100644 app/livechat/server/methods/saveOfficeHours.js rename {packages/rocketchat-livechat => app/livechat}/server/methods/saveSurveyFeedback.js (75%) create mode 100644 app/livechat/server/methods/saveTrigger.js create mode 100644 app/livechat/server/methods/searchAgent.js rename {packages/rocketchat-livechat => app/livechat}/server/methods/sendFileLivechatMessage.js (83%) create mode 100644 app/livechat/server/methods/sendMessageLivechat.js create mode 100644 app/livechat/server/methods/sendOfflineMessage.js create mode 100644 app/livechat/server/methods/sendTranscript.js create mode 100644 app/livechat/server/methods/setCustomField.js create mode 100644 app/livechat/server/methods/setDepartmentForVisitor.js create mode 100644 app/livechat/server/methods/setUpConnection.js create mode 100644 app/livechat/server/methods/startFileUploadRoom.js create mode 100644 app/livechat/server/methods/startVideoCall.js create mode 100644 app/livechat/server/methods/takeInquiry.js create mode 100644 app/livechat/server/methods/transfer.js rename {packages/rocketchat-livechat => app/livechat}/server/methods/webhookTest.js (80%) create mode 100644 app/livechat/server/permissions.js create mode 100644 app/livechat/server/publications/customFields.js create mode 100644 app/livechat/server/publications/departmentAgents.js create mode 100644 app/livechat/server/publications/externalMessages.js create mode 100644 app/livechat/server/publications/livechatAgents.js create mode 100644 app/livechat/server/publications/livechatAppearance.js create mode 100644 app/livechat/server/publications/livechatDepartments.js create mode 100644 app/livechat/server/publications/livechatInquiries.js create mode 100644 app/livechat/server/publications/livechatIntegration.js create mode 100644 app/livechat/server/publications/livechatManagers.js create mode 100644 app/livechat/server/publications/livechatMonitoring.js create mode 100644 app/livechat/server/publications/livechatOfficeHours.js create mode 100644 app/livechat/server/publications/livechatQueue.js rename {packages/rocketchat-livechat => app/livechat}/server/publications/livechatRooms.js (79%) create mode 100644 app/livechat/server/publications/livechatTriggers.js rename {packages/rocketchat-livechat => app/livechat}/server/publications/livechatVisitors.js (76%) create mode 100644 app/livechat/server/publications/visitorHistory.js create mode 100644 app/livechat/server/publications/visitorInfo.js create mode 100644 app/livechat/server/publications/visitorPageVisited.js create mode 100644 app/livechat/server/roomType.js create mode 100644 app/livechat/server/sendMessageBySMS.js create mode 100644 app/livechat/server/startup.js create mode 100644 app/livechat/server/unclosedLivechats.js create mode 100644 app/livechat/server/visitorStatus.js rename {packages/rocketchat-livestream => app/livestream}/.gitignore (100%) create mode 100644 app/livestream/client/index.js create mode 100644 app/livestream/client/oauth.js rename {packages/rocketchat-livestream => app/livestream}/client/styles/liveStreamTab.css (100%) create mode 100644 app/livestream/client/tabBar.js rename {packages/rocketchat-livestream => app/livestream}/client/views/broadcastView.html (100%) rename {packages/rocketchat-livestream => app/livestream}/client/views/broadcastView.js (83%) rename {packages/rocketchat-livestream => app/livestream}/client/views/liveStreamTab.html (100%) rename {packages/rocketchat-livestream => app/livestream}/client/views/liveStreamTab.js (87%) rename {packages/rocketchat-livestream => app/livestream}/client/views/liveStreamView.html (100%) rename {packages/rocketchat-livestream => app/livestream}/client/views/liveStreamView.js (94%) rename {packages/rocketchat-livestream => app/livestream}/client/views/livestreamBroadcast.html (100%) rename {packages/rocketchat-livestream => app/livestream}/client/views/livestreamBroadcast.js (100%) rename {packages/rocketchat-livestream => app/livestream}/server/functions/livestream.js (82%) create mode 100644 app/livestream/server/index.js create mode 100644 app/livestream/server/methods.js create mode 100644 app/livestream/server/routes.js create mode 100644 app/livestream/server/settings.js rename {packages/rocketchat-logger => app/logger}/README.md (100%) create mode 100644 app/logger/client/ansispan.js create mode 100644 app/logger/client/index.js create mode 100644 app/logger/client/logger.js create mode 100644 app/logger/client/viewLogs.js create mode 100644 app/logger/client/views/viewLogs.css create mode 100644 app/logger/client/views/viewLogs.html create mode 100644 app/logger/client/views/viewLogs.js create mode 100644 app/logger/index.js create mode 100644 app/logger/server/index.js create mode 100644 app/logger/server/server.js create mode 100644 app/mail-messages/client/index.js create mode 100644 app/mail-messages/client/router.js create mode 100644 app/mail-messages/client/startup.js rename {packages/rocketchat-mailer => app/mail-messages}/client/views/mailer.html (100%) create mode 100644 app/mail-messages/client/views/mailer.js rename {packages/rocketchat-mailer => app/mail-messages}/client/views/mailerUnsubscribe.html (100%) create mode 100644 app/mail-messages/client/views/mailerUnsubscribe.js create mode 100644 app/mail-messages/server/functions/sendMail.js create mode 100644 app/mail-messages/server/functions/unsubscribe.js create mode 100644 app/mail-messages/server/index.js create mode 100644 app/mail-messages/server/lib/Mailer.js create mode 100644 app/mail-messages/server/methods/sendMail.js create mode 100644 app/mail-messages/server/methods/unsubscribe.js create mode 100644 app/mail-messages/server/startup.js create mode 100644 app/mailer/index.js create mode 100644 app/mailer/server/api.js create mode 100644 app/mapview/client/index.js rename {packages/rocketchat-mapview => app/mapview}/client/mapview.js (77%) create mode 100644 app/mapview/server/index.js create mode 100644 app/mapview/server/settings.js create mode 100644 app/markdown/client/index.js create mode 100644 app/markdown/lib/markdown.js create mode 100644 app/markdown/lib/parser/marked/marked.js rename {packages/rocketchat-markdown => app/markdown/lib}/parser/original/code.js (91%) create mode 100644 app/markdown/lib/parser/original/markdown.js rename {packages/rocketchat-markdown => app/markdown/lib}/parser/original/original.js (100%) create mode 100644 app/markdown/server/index.js create mode 100644 app/markdown/server/settings.js create mode 100644 app/markdown/tests/client.mocks.js create mode 100644 app/markdown/tests/client.tests.js create mode 100644 app/mentions-flextab/client/actionButton.js create mode 100644 app/mentions-flextab/client/index.js create mode 100644 app/mentions-flextab/client/lib/MentionedMessage.js create mode 100644 app/mentions-flextab/client/tabBar.js create mode 100644 app/mentions-flextab/client/views/mentionsFlexTab.html create mode 100644 app/mentions-flextab/client/views/mentionsFlexTab.js create mode 100644 app/mentions-flextab/server/index.js create mode 100644 app/mentions-flextab/server/publications/mentionedMessages.js create mode 100644 app/mentions/client/client.js create mode 100644 app/mentions/client/index.js create mode 100644 app/mentions/client/mentionLink.css create mode 100644 app/mentions/lib/MentionsParser.js create mode 100644 app/mentions/server/Mentions.js create mode 100644 app/mentions/server/index.js create mode 100644 app/mentions/server/methods/getUserMentionsByChannel.js create mode 100644 app/mentions/server/server.js create mode 100644 app/mentions/tests/client.tests.js create mode 100644 app/mentions/tests/server.tests.js create mode 100644 app/message-action/client/index.js rename {packages/rocketchat-message-action => app/message-action}/client/messageAction.html (81%) create mode 100644 app/message-action/client/messageAction.js rename {packages/rocketchat-message-action => app/message-action}/client/stylesheets/messageAction.css (100%) create mode 100644 app/message-action/index.js create mode 100644 app/message-attachments/client/index.js create mode 100644 app/message-attachments/client/messageAttachment.html create mode 100644 app/message-attachments/client/messageAttachment.js create mode 100644 app/message-attachments/client/renderField.html create mode 100644 app/message-attachments/client/renderField.js rename {packages/rocketchat-message-attachments => app/message-attachments}/client/stylesheets/messageAttachments.css (89%) create mode 100644 app/message-attachments/index.js create mode 100644 app/message-mark-as-unread/client/actionButton.js create mode 100644 app/message-mark-as-unread/client/index.js create mode 100644 app/message-mark-as-unread/server/index.js create mode 100644 app/message-mark-as-unread/server/logger.js create mode 100644 app/message-mark-as-unread/server/unreadMessages.js create mode 100644 app/message-pin/client/actionButton.js create mode 100644 app/message-pin/client/index.js create mode 100644 app/message-pin/client/lib/PinnedMessage.js create mode 100644 app/message-pin/client/messageType.js create mode 100644 app/message-pin/client/pinMessage.js create mode 100644 app/message-pin/client/tabBar.js create mode 100644 app/message-pin/client/views/pinnedMessages.html create mode 100644 app/message-pin/client/views/pinnedMessages.js rename {packages/rocketchat-message-pin => app/message-pin}/client/views/stylesheets/messagepin.css (100%) create mode 100644 app/message-pin/server/index.js create mode 100644 app/message-pin/server/pinMessage.js create mode 100644 app/message-pin/server/publications/pinnedMessages.js create mode 100644 app/message-pin/server/settings.js create mode 100644 app/message-pin/server/startup/indexes.js create mode 100644 app/message-snippet/client/actionButton.js create mode 100644 app/message-snippet/client/index.js create mode 100644 app/message-snippet/client/lib/collections.js create mode 100644 app/message-snippet/client/messageType.js rename {packages/rocketchat-message-snippet => app/message-snippet}/client/page/snippetPage.html (100%) create mode 100644 app/message-snippet/client/page/snippetPage.js rename {packages/rocketchat-message-snippet => app/message-snippet}/client/page/stylesheets/snippetPage.css (100%) create mode 100644 app/message-snippet/client/router.js create mode 100644 app/message-snippet/client/snippetMessage.js create mode 100644 app/message-snippet/client/tabBar/tabBar.js create mode 100644 app/message-snippet/client/tabBar/views/snippetedMessages.html create mode 100644 app/message-snippet/client/tabBar/views/snippetedMessages.js create mode 100644 app/message-snippet/server/index.js create mode 100644 app/message-snippet/server/methods/snippetMessage.js create mode 100644 app/message-snippet/server/publications/snippetedMessage.js create mode 100644 app/message-snippet/server/publications/snippetedMessagesByRoom.js create mode 100644 app/message-snippet/server/requests.js create mode 100644 app/message-snippet/server/startup/settings.js create mode 100644 app/message-star/client/actionButton.js create mode 100644 app/message-star/client/index.js create mode 100644 app/message-star/client/lib/StarredMessage.js create mode 100644 app/message-star/client/starMessage.js create mode 100644 app/message-star/client/tabBar.js create mode 100644 app/message-star/client/views/starredMessages.html create mode 100644 app/message-star/client/views/starredMessages.js rename {packages/rocketchat-message-star => app/message-star}/client/views/stylesheets/messagestar.css (100%) create mode 100644 app/message-star/server/index.js create mode 100644 app/message-star/server/publications/starredMessages.js create mode 100644 app/message-star/server/settings.js create mode 100644 app/message-star/server/starMessage.js create mode 100644 app/message-star/server/startup/indexes.js rename {packages => app}/meteor-accounts-saml/CHANGELOG.md (100%) rename {packages => app}/meteor-accounts-saml/README.md (100%) create mode 100644 app/meteor-accounts-saml/client/index.js create mode 100644 app/meteor-accounts-saml/client/saml_client.js create mode 100644 app/meteor-accounts-saml/server/index.js create mode 100644 app/meteor-accounts-saml/server/saml_rocketchat.js create mode 100644 app/meteor-accounts-saml/server/saml_server.js create mode 100644 app/meteor-accounts-saml/server/saml_utils.js create mode 100644 app/metrics/index.js create mode 100644 app/metrics/server/callbacksMetrics.js create mode 100644 app/metrics/server/index.js create mode 100644 app/metrics/server/lib/metrics.js rename {packages/rocketchat-lib/server/startup => app/metrics/server/lib}/statsTracker.js (87%) create mode 100644 app/migrations/index.js create mode 100644 app/migrations/server/index.js rename {packages/rocketchat-migrations => app/migrations/server}/migrations.js (91%) create mode 100644 app/models/client/index.js create mode 100644 app/models/client/models/Avatars.js create mode 100644 app/models/client/models/CachedChannelList.js create mode 100644 app/models/client/models/CachedChatRoom.js create mode 100644 app/models/client/models/CachedChatSubscription.js create mode 100644 app/models/client/models/CachedUserList.js create mode 100644 app/models/client/models/ChatMessage.js create mode 100644 app/models/client/models/ChatPermissions.js create mode 100644 app/models/client/models/ChatRoom.js create mode 100644 app/models/client/models/ChatSubscription.js create mode 100644 app/models/client/models/CustomSounds.js create mode 100644 app/models/client/models/CustomUserStatus.js create mode 100644 app/models/client/models/EmojiCustom.js create mode 100644 app/models/client/models/FullUser.js create mode 100644 app/models/client/models/Roles.js create mode 100644 app/models/client/models/RoomRoles.js create mode 100644 app/models/client/models/Subscriptions.js create mode 100644 app/models/client/models/Uploads.js create mode 100644 app/models/client/models/UserAndRoom.js create mode 100644 app/models/client/models/UserDataFiles.js create mode 100644 app/models/client/models/UserRoles.js create mode 100644 app/models/client/models/Users.js create mode 100644 app/models/client/models/WebdavAccounts.js create mode 100644 app/models/client/models/_Base.js create mode 100644 app/models/index.js create mode 100644 app/models/server/index.js create mode 100644 app/models/server/models/Avatars.js rename {packages/rocketchat-cas => app/models}/server/models/CredentialTokens.js (80%) create mode 100644 app/models/server/models/CustomSounds.js create mode 100644 app/models/server/models/CustomUserStatus.js create mode 100644 app/models/server/models/EmojiCustom.js rename {packages/rocketchat-lib => app/models}/server/models/ExportOperations.js (75%) create mode 100644 app/models/server/models/FederationDNSCache.js create mode 100644 app/models/server/models/FederationEvents.js create mode 100644 app/models/server/models/FederationKeys.js create mode 100644 app/models/server/models/FederationPeers.js rename {packages/rocketchat-integrations => app/models}/server/models/IntegrationHistory.js (84%) create mode 100644 app/models/server/models/Integrations.js create mode 100644 app/models/server/models/LivechatCustomField.js create mode 100644 app/models/server/models/LivechatDepartment.js create mode 100644 app/models/server/models/LivechatDepartmentAgents.js rename {packages/rocketchat-livechat => app/models}/server/models/LivechatOfficeHour.js (77%) create mode 100644 app/models/server/models/LivechatPageVisited.js create mode 100644 app/models/server/models/LivechatTrigger.js rename {packages/rocketchat-livechat => app/models}/server/models/LivechatVisitors.js (90%) create mode 100644 app/models/server/models/Messages.js create mode 100644 app/models/server/models/OAuthApps.js rename {packages/rocketchat-oembed => app/models}/server/models/OEmbedCache.js (78%) create mode 100644 app/models/server/models/Permissions.js create mode 100644 app/models/server/models/ReadReceipts.js rename {packages/rocketchat-lib => app/models}/server/models/Reports.js (75%) create mode 100644 app/models/server/models/Roles.js create mode 100644 app/models/server/models/Rooms.js create mode 100644 app/models/server/models/Sessions.js create mode 100644 app/models/server/models/Sessions.mocks.js create mode 100644 app/models/server/models/Sessions.tests.js create mode 100644 app/models/server/models/Settings.js create mode 100644 app/models/server/models/SmarshHistory.js rename {packages/rocketchat-statistics => app/models}/server/models/Statistics.js (77%) create mode 100644 app/models/server/models/Subscriptions.js create mode 100644 app/models/server/models/Uploads.js create mode 100644 app/models/server/models/UserDataFiles.js create mode 100644 app/models/server/models/Users.js create mode 100644 app/models/server/models/WebdavAccounts.js create mode 100644 app/models/server/models/_Base.js create mode 100644 app/models/server/models/_BaseDb.js create mode 100644 app/models/server/models/apps-logs-model.js create mode 100644 app/models/server/models/apps-model.js create mode 100644 app/models/server/models/apps-persistence-model.js create mode 100644 app/models/server/raw/BaseRaw.js create mode 100644 app/models/server/raw/Permissions.js create mode 100644 app/models/server/raw/Roles.js create mode 100644 app/models/server/raw/Rooms.js create mode 100644 app/models/server/raw/Settings.js create mode 100644 app/models/server/raw/Subscriptions.js create mode 100644 app/models/server/raw/Users.js create mode 100644 app/models/server/raw/index.js create mode 100644 app/notifications/client/index.js create mode 100644 app/notifications/client/lib/Notifications.js create mode 100644 app/notifications/index.js create mode 100644 app/notifications/server/index.js create mode 100644 app/notifications/server/lib/Notifications.js rename {packages/rocketchat-nrr => app/nrr}/README.md (100%) create mode 100644 app/nrr/client/index.js rename {packages/rocketchat-nrr => app/nrr/client}/nrr.js (77%) create mode 100644 app/nrr/index.js rename {packages/rocketchat-custom-oauth => app/oauth2-server-config}/.gitignore (100%) create mode 100644 app/oauth2-server-config/client/admin/collection.js create mode 100644 app/oauth2-server-config/client/admin/route.js create mode 100644 app/oauth2-server-config/client/admin/startup.js rename {packages/rocketchat-oauth2-server-config/admin/client => app/oauth2-server-config/client/admin}/views/oauthApp.html (96%) rename {packages/rocketchat-oauth2-server-config/admin/client => app/oauth2-server-config/client/admin}/views/oauthApp.js (75%) create mode 100644 app/oauth2-server-config/client/admin/views/oauthApps.html create mode 100644 app/oauth2-server-config/client/admin/views/oauthApps.js create mode 100644 app/oauth2-server-config/client/index.js rename {packages/rocketchat-oauth2-server-config/oauth/client => app/oauth2-server-config/client/oauth}/oauth2-client.html (100%) rename {packages/rocketchat-oauth2-server-config/oauth/client => app/oauth2-server-config/client/oauth}/oauth2-client.js (76%) rename {packages/rocketchat-oauth2-server-config/oauth/client => app/oauth2-server-config/client/oauth}/stylesheets/oauth2.css (100%) create mode 100644 app/oauth2-server-config/server/admin/functions/parseUriList.js create mode 100644 app/oauth2-server-config/server/admin/methods/addOAuthApp.js create mode 100644 app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js create mode 100644 app/oauth2-server-config/server/admin/methods/updateOAuthApp.js create mode 100644 app/oauth2-server-config/server/admin/publications/oauthApps.js create mode 100644 app/oauth2-server-config/server/index.js create mode 100644 app/oauth2-server-config/server/oauth/default-services.js rename {packages/rocketchat-oauth2-server-config/oauth/server => app/oauth2-server-config/server/oauth}/oauth2-server.js (79%) rename {packages/rocketchat-oembed => app/oembed}/client/baseWidget.html (100%) rename {packages/rocketchat-oembed => app/oembed}/client/baseWidget.js (87%) create mode 100644 app/oembed/client/index.js rename {packages/rocketchat-oembed => app/oembed}/client/oembedAudioWidget.html (88%) create mode 100644 app/oembed/client/oembedAudioWidget.js rename {packages/rocketchat-oembed => app/oembed}/client/oembedFrameWidget.html (93%) create mode 100644 app/oembed/client/oembedFrameWidget.js rename {packages/rocketchat-oembed => app/oembed}/client/oembedImageWidget.html (100%) create mode 100644 app/oembed/client/oembedImageWidget.js rename {packages/rocketchat-oembed => app/oembed}/client/oembedUrlWidget.html (93%) rename {packages/rocketchat-oembed => app/oembed}/client/oembedUrlWidget.js (87%) rename {packages/rocketchat-oembed => app/oembed}/client/oembedVideoWidget.html (89%) create mode 100644 app/oembed/client/oembedVideoWidget.js rename {packages/rocketchat-oembed => app/oembed}/client/oembedYoutubeWidget.html (79%) create mode 100644 app/oembed/client/oembedYoutubeWidget.js create mode 100644 app/oembed/server/index.js create mode 100644 app/oembed/server/jumpToMessage.js create mode 100644 app/oembed/server/providers.js create mode 100644 app/oembed/server/server.js create mode 100644 app/otr/client/index.js create mode 100644 app/otr/client/rocketchat.otr.js rename {packages/rocketchat-otr => app/otr}/client/rocketchat.otr.room.js (80%) rename {packages/rocketchat-otr => app/otr}/client/stylesheets/otr.css (100%) create mode 100644 app/otr/client/tabBar.js rename {packages/rocketchat-otr => app/otr}/client/views/otrFlexTab.html (100%) create mode 100644 app/otr/client/views/otrFlexTab.js create mode 100644 app/otr/server/index.js create mode 100644 app/otr/server/methods/deleteOldOTRMessages.js create mode 100644 app/otr/server/methods/updateOTRAck.js create mode 100644 app/otr/server/settings.js create mode 100644 app/promises/client/index.js create mode 100644 app/promises/lib/promises.js create mode 100644 app/promises/server/index.js create mode 100644 app/push-notifications/client/index.js rename {packages/rocketchat-push-notifications => app/push-notifications}/client/stylesheets/pushNotifications.css (100%) create mode 100644 app/push-notifications/client/tabBar.js rename {packages/rocketchat-push-notifications => app/push-notifications}/client/views/pushNotificationsFlexTab.html (99%) rename {packages/rocketchat-push-notifications => app/push-notifications}/client/views/pushNotificationsFlexTab.js (81%) create mode 100644 app/push-notifications/server/index.js create mode 100644 app/push-notifications/server/lib/PushNotification.js create mode 100644 app/push-notifications/server/methods/saveNotificationSettings.js rename {packages/rocketchat-reactions => app/reactions}/README.md (100%) create mode 100644 app/reactions/client/index.js create mode 100644 app/reactions/client/init.js create mode 100644 app/reactions/client/methods/setReaction.js rename {packages/rocketchat-reactions => app/reactions}/client/stylesheets/reaction.css (98%) create mode 100644 app/reactions/server/index.js create mode 100644 app/reactions/server/setReaction.js rename {packages/rocketchat-retention-policy => app/retention-policy}/README.md (100%) create mode 100644 app/retention-policy/index.js create mode 100644 app/retention-policy/server/cronPruneMessages.js create mode 100644 app/retention-policy/server/index.js create mode 100644 app/retention-policy/server/startup/settings.js rename {packages/rocketchat-search => app/search}/README.md (100%) create mode 100644 app/search/client/index.js create mode 100644 app/search/client/provider/result.html create mode 100644 app/search/client/provider/result.js rename {packages/rocketchat-search => app/search}/client/provider/suggestion.html (100%) rename {packages/rocketchat-search => app/search}/client/search/search.html (100%) rename {packages/rocketchat-search => app/search}/client/search/search.js (84%) rename {packages/rocketchat-search => app/search}/client/style/style.css (98%) create mode 100644 app/search/server/events/events.js create mode 100644 app/search/server/index.js create mode 100644 app/search/server/logger/logger.js create mode 100644 app/search/server/model/provider.js rename {packages/rocketchat-search => app/search}/server/provider/defaultProvider.js (96%) rename {packages/rocketchat-search => app/search}/server/service/providerService.js (95%) rename {packages/rocketchat-search => app/search}/server/service/validationService.js (92%) create mode 100644 app/settings/client/index.js create mode 100644 app/settings/client/lib/settings.js create mode 100644 app/settings/index.js create mode 100644 app/settings/lib/settings.js create mode 100644 app/settings/server/functions/settings.js create mode 100644 app/settings/server/index.js create mode 100644 app/settings/server/observer.js create mode 100644 app/settings/server/raw.js rename {packages/rocketchat-setup-wizard => app/setup-wizard}/client/final.html (100%) create mode 100644 app/setup-wizard/client/final.js create mode 100644 app/setup-wizard/client/index.js rename {packages/rocketchat-setup-wizard => app/setup-wizard}/client/setupWizard.html (99%) create mode 100644 app/setup-wizard/client/setupWizard.js create mode 100644 app/setup-wizard/server/getSetupWizardParameters.js create mode 100644 app/setup-wizard/server/index.js rename {packages/rocketchat-slackbridge => app/slackbridge}/README.md (100%) create mode 100644 app/slackbridge/client/index.js create mode 100644 app/slackbridge/client/slackbridge_import.client.js create mode 100644 app/slackbridge/server/RocketAdapter.js create mode 100644 app/slackbridge/server/SlackAPI.js rename {packages/rocketchat-slackbridge => app/slackbridge}/server/SlackAdapter.js (75%) create mode 100644 app/slackbridge/server/index.js create mode 100644 app/slackbridge/server/logger.js create mode 100644 app/slackbridge/server/settings.js create mode 100644 app/slackbridge/server/slackbridge.js create mode 100644 app/slackbridge/server/slackbridge_import.server.js rename {packages/rocketchat-slackbridge => app/slackbridge}/tests/manual-tests.txt (100%) create mode 100644 app/slashcommand-asciiarts/client/index.js rename {packages/rocketchat-slashcommand-asciiarts => app/slashcommand-asciiarts/lib}/gimme.js (75%) create mode 100644 app/slashcommand-asciiarts/lib/lenny.js rename {packages/rocketchat-slashcommand-asciiarts => app/slashcommand-asciiarts/lib}/shrug.js (75%) rename {packages/rocketchat-slashcommand-asciiarts => app/slashcommand-asciiarts/lib}/tableflip.js (75%) rename {packages/rocketchat-slashcommand-asciiarts => app/slashcommand-asciiarts/lib}/unflip.js (75%) create mode 100644 app/slashcommand-asciiarts/server/index.js create mode 100644 app/slashcommands-archiveroom/client/client.js create mode 100644 app/slashcommands-archiveroom/client/index.js create mode 100644 app/slashcommands-archiveroom/server/index.js create mode 100644 app/slashcommands-archiveroom/server/server.js create mode 100644 app/slashcommands-create/client/client.js create mode 100644 app/slashcommands-create/client/index.js create mode 100644 app/slashcommands-create/server/index.js create mode 100644 app/slashcommands-create/server/server.js create mode 100644 app/slashcommands-help/index.js create mode 100644 app/slashcommands-help/server/index.js create mode 100644 app/slashcommands-help/server/server.js create mode 100644 app/slashcommands-hide/client/hide.js create mode 100644 app/slashcommands-hide/client/index.js create mode 100644 app/slashcommands-hide/server/hide.js create mode 100644 app/slashcommands-hide/server/index.js create mode 100644 app/slashcommands-invite/client/client.js create mode 100644 app/slashcommands-invite/client/index.js create mode 100644 app/slashcommands-invite/server/index.js create mode 100644 app/slashcommands-invite/server/server.js create mode 100644 app/slashcommands-inviteall/client/client.js create mode 100644 app/slashcommands-inviteall/client/index.js create mode 100644 app/slashcommands-inviteall/server/index.js create mode 100644 app/slashcommands-inviteall/server/server.js create mode 100644 app/slashcommands-join/client/client.js create mode 100644 app/slashcommands-join/client/index.js create mode 100644 app/slashcommands-join/server/index.js create mode 100644 app/slashcommands-join/server/server.js create mode 100644 app/slashcommands-kick/client/client.js create mode 100644 app/slashcommands-kick/client/index.js create mode 100644 app/slashcommands-kick/server/index.js create mode 100644 app/slashcommands-kick/server/server.js create mode 100644 app/slashcommands-leave/index.js create mode 100644 app/slashcommands-leave/server/index.js create mode 100644 app/slashcommands-leave/server/leave.js create mode 100644 app/slashcommands-me/index.js create mode 100644 app/slashcommands-me/server/index.js create mode 100644 app/slashcommands-me/server/me.js create mode 100644 app/slashcommands-msg/index.js create mode 100644 app/slashcommands-msg/server/index.js create mode 100644 app/slashcommands-msg/server/server.js create mode 100644 app/slashcommands-mute/index.js create mode 100644 app/slashcommands-mute/server/index.js create mode 100644 app/slashcommands-mute/server/mute.js create mode 100644 app/slashcommands-mute/server/unmute.js create mode 100644 app/slashcommands-open/client/client.js create mode 100644 app/slashcommands-open/client/index.js create mode 100644 app/slashcommands-open/index.js create mode 100644 app/slashcommands-status/client/index.js create mode 100644 app/slashcommands-status/index.js create mode 100644 app/slashcommands-status/lib/status.js create mode 100644 app/slashcommands-status/server/index.js create mode 100644 app/slashcommands-topic/client/index.js create mode 100644 app/slashcommands-topic/lib/topic.js create mode 100644 app/slashcommands-topic/server/index.js create mode 100644 app/slashcommands-unarchiveroom/client/client.js create mode 100644 app/slashcommands-unarchiveroom/client/index.js create mode 100644 app/slashcommands-unarchiveroom/server/index.js create mode 100644 app/slashcommands-unarchiveroom/server/server.js rename {packages/rocketchat-slider => app/slider}/README.md (100%) create mode 100644 app/slider/client/index.js rename {packages/rocketchat-slider => app/slider/client}/rocketchat-slider.html (100%) rename {packages/rocketchat-slider => app/slider/client}/rocketchat-slider.js (87%) create mode 100644 app/slider/index.js create mode 100644 app/smarsh-connector/index.js rename {packages/rocketchat-smarsh-connector => app/smarsh-connector}/server/functions/generateEml.js (77%) create mode 100644 app/smarsh-connector/server/functions/sendEmail.js create mode 100644 app/smarsh-connector/server/index.js create mode 100644 app/smarsh-connector/server/lib/rocketchat.js create mode 100644 app/smarsh-connector/server/settings.js create mode 100644 app/smarsh-connector/server/startup.js rename {packages/rocketchat-sms => app/sms}/README.md (100%) create mode 100644 app/sms/index.js create mode 100644 app/sms/server/SMS.js create mode 100644 app/sms/server/index.js rename {packages/rocketchat-sms => app/sms/server}/services/twilio.js (86%) create mode 100644 app/sms/server/services/voxtelesys.js create mode 100644 app/sms/server/settings.js create mode 100644 app/spotify/client/index.js rename {packages/rocketchat-spotify => app/spotify}/lib/client/oembedSpotifyWidget.html (92%) rename {packages/rocketchat-spotify => app/spotify}/lib/client/widget.js (83%) rename {packages/rocketchat-spotify => app/spotify}/lib/spotify.js (82%) create mode 100644 app/spotify/server/index.js create mode 100644 app/statistics/index.js create mode 100644 app/statistics/server/functions/get.js create mode 100644 app/statistics/server/functions/save.js create mode 100644 app/statistics/server/index.js create mode 100644 app/statistics/server/lib/SAUMonitor.js create mode 100644 app/statistics/server/lib/UAParserCustom.js create mode 100644 app/statistics/server/lib/UAParserCustom.tests.js create mode 100644 app/statistics/server/methods/getStatistics.js create mode 100644 app/statistics/server/startup/monitor.js create mode 100644 app/statistics/server/statisticsNamespace.js rename {packages/rocketchat-theme => app/theme}/client/imports/components/alerts.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/avatar.css (100%) create mode 100644 app/theme/client/imports/components/badge.css rename {packages/rocketchat-theme => app/theme}/client/imports/components/chip.css (100%) create mode 100644 app/theme/client/imports/components/contextual-bar.css rename {packages/rocketchat-theme => app/theme}/client/imports/components/emojiPicker.css (89%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/flex-nav.css (100%) create mode 100644 app/theme/client/imports/components/header.css rename {packages/rocketchat-theme => app/theme}/client/imports/components/main-content.css (94%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/memberlist.css (100%) create mode 100644 app/theme/client/imports/components/message-box.css create mode 100644 app/theme/client/imports/components/messages.css rename {packages/rocketchat-theme => app/theme}/client/imports/components/modal.css (90%) create mode 100644 app/theme/client/imports/components/modal/create-channel.css rename {packages/rocketchat-theme => app/theme}/client/imports/components/modal/full-modal.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/popout.css (97%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/popover.css (91%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/setup-wizard.css (99%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/sidebar/rooms-list.css (99%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/sidebar/sidebar-flex.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/sidebar/sidebar-header.css (87%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/sidebar/sidebar-item.css (88%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/sidebar/sidebar.css (96%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/sidebar/toolbar.css (99%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/slider.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/table.css (91%) create mode 100644 app/theme/client/imports/components/tabs.css rename {packages/rocketchat-theme => app/theme}/client/imports/components/tooltip.css (83%) rename {packages/rocketchat-theme => app/theme}/client/imports/components/userInfo.css (98%) create mode 100644 app/theme/client/imports/forms/button.css rename {packages/rocketchat-theme => app/theme}/client/imports/forms/checkbox.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/forms/input.css (89%) rename {packages/rocketchat-theme => app/theme}/client/imports/forms/popup-list.css (92%) rename {packages/rocketchat-theme => app/theme}/client/imports/forms/select-avatar.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/forms/select.css (76%) rename {packages/rocketchat-theme => app/theme}/client/imports/forms/switch.css (100%) rename {packages/rocketchat-theme => app/theme}/client/imports/forms/tags.css (95%) rename {packages/rocketchat-theme => app/theme}/client/imports/general/animations.css (97%) create mode 100644 app/theme/client/imports/general/apps.css rename {packages/rocketchat-theme => app/theme}/client/imports/general/base.css (75%) rename {packages/rocketchat-theme => app/theme}/client/imports/general/base_old.css (85%) rename {packages/rocketchat-theme => app/theme}/client/imports/general/forms.css (99%) rename {packages/rocketchat-theme => app/theme}/client/imports/general/reset.css (97%) create mode 100644 app/theme/client/imports/general/rtl.css rename {packages/rocketchat-theme => app/theme}/client/imports/general/typography.css (100%) create mode 100644 app/theme/client/imports/general/variables.css create mode 100644 app/theme/client/index.js rename {packages/rocketchat-theme => app/theme}/client/main.css (97%) rename {packages/rocketchat-theme => app/theme}/client/vendor/fontello/config.json (100%) rename {packages/rocketchat-theme => app/theme}/client/vendor/fontello/css/fontello.css (100%) rename {packages/rocketchat-theme => app/theme}/client/vendor/jscolor.js (100%) rename {packages/rocketchat-theme => app/theme}/client/vendor/photoswipe.css (100%) create mode 100644 app/theme/server/index.js create mode 100644 app/theme/server/server.js rename {packages/rocketchat-theme => app/theme}/server/variables.js (76%) create mode 100644 app/threads/README.md create mode 100644 app/threads/client/flextab/thread.html create mode 100644 app/threads/client/flextab/thread.js create mode 100644 app/threads/client/flextab/threadlist.js create mode 100644 app/threads/client/flextab/threads.html create mode 100644 app/threads/client/flextab/threads.js create mode 100644 app/threads/client/index.js create mode 100644 app/threads/client/messageAction/follow.js create mode 100644 app/threads/client/messageAction/replyInThread.js create mode 100644 app/threads/client/messageAction/unfollow.js create mode 100644 app/threads/client/threads.css create mode 100644 app/threads/client/upsert.js create mode 100644 app/threads/server/functions.js create mode 100644 app/threads/server/hooks/afterReadMessages.js create mode 100644 app/threads/server/hooks/afterdeletemessage.js create mode 100644 app/threads/server/hooks/aftersavemessage.js create mode 100644 app/threads/server/hooks/index.js create mode 100644 app/threads/server/index.js create mode 100644 app/threads/server/methods/followMessage.js create mode 100644 app/threads/server/methods/getThreadMessages.js create mode 100644 app/threads/server/methods/getThreadsList.js create mode 100644 app/threads/server/methods/index.js create mode 100644 app/threads/server/methods/unfollowMessage.js create mode 100644 app/threads/server/settings.js create mode 100644 app/token-login/client/index.js create mode 100644 app/token-login/client/login_token_client.js create mode 100644 app/token-login/server/index.js rename {packages/rocketchat-token-login => app/token-login}/server/login_token_server.js (79%) rename {packages/rocketchat-tokenpass => app/tokenpass}/README.md (100%) rename {packages/rocketchat-tokenpass => app/tokenpass}/client/channelSettings.css (100%) create mode 100644 app/tokenpass/client/index.js rename {packages/rocketchat-tokenpass => app/tokenpass}/client/login-button.css (100%) create mode 100644 app/tokenpass/client/roomType.js create mode 100644 app/tokenpass/client/startup.js rename {packages/rocketchat-tokenpass => app/tokenpass}/client/styles.css (100%) rename {packages/rocketchat-tokenpass => app/tokenpass}/client/tokenChannelsList.html (100%) create mode 100644 app/tokenpass/client/tokenChannelsList.js rename {packages/rocketchat-tokenpass => app/tokenpass}/client/tokenpassChannelSettings.html (100%) rename {packages/rocketchat-tokenpass => app/tokenpass}/client/tokenpassChannelSettings.js (82%) create mode 100644 app/tokenpass/lib/common.js rename {packages/rocketchat-tokenpass => app/tokenpass}/server/Tokenpass.js (91%) create mode 100644 app/tokenpass/server/cronRemoveUsers.js create mode 100644 app/tokenpass/server/functions/getProtectedTokenpassBalances.js create mode 100644 app/tokenpass/server/functions/getPublicTokenpassBalances.js create mode 100644 app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js create mode 100644 app/tokenpass/server/functions/updateUserTokenpassBalances.js create mode 100644 app/tokenpass/server/index.js create mode 100644 app/tokenpass/server/methods/findTokenChannels.js create mode 100644 app/tokenpass/server/methods/getChannelTokenpass.js create mode 100644 app/tokenpass/server/startup.js rename {packages/rocketchat-tooltip => app/tooltip}/README.md (100%) create mode 100644 app/tooltip/client/index.js rename {packages/rocketchat-tooltip => app/tooltip}/client/rocketchat-tooltip.html (100%) rename {packages/rocketchat-tooltip => app/tooltip}/client/rocketchat-tooltip.js (93%) rename {packages/rocketchat-tooltip => app/tooltip}/client/tooltip.css (100%) create mode 100644 app/tooltip/index.js rename {packages/rocketchat-ui-account => app/ui-account}/README.md (100%) rename {packages/rocketchat-ui-account => app/ui-account}/client/account.html (100%) create mode 100644 app/ui-account/client/account.js rename {packages/rocketchat-ui-account => app/ui-account}/client/accountFlex.html (79%) create mode 100644 app/ui-account/client/accountFlex.js create mode 100644 app/ui-account/client/accountIntegrations.html create mode 100644 app/ui-account/client/accountIntegrations.js create mode 100644 app/ui-account/client/accountPreferences.html create mode 100644 app/ui-account/client/accountPreferences.js rename {packages/rocketchat-ui-account => app/ui-account}/client/accountProfile.html (89%) rename {packages/rocketchat-ui-account => app/ui-account}/client/accountProfile.js (81%) rename {packages/rocketchat-ui-account => app/ui-account}/client/avatar/avatar.html (100%) create mode 100644 app/ui-account/client/avatar/avatar.js rename {packages/rocketchat-ui-account => app/ui-account}/client/avatar/prompt.html (100%) rename {packages/rocketchat-ui-account => app/ui-account}/client/avatar/prompt.js (79%) create mode 100644 app/ui-account/client/index.js create mode 100644 app/ui-account/index.js rename {packages/rocketchat-ui-admin => app/ui-admin}/README.md (100%) create mode 100644 app/ui-admin/client/SettingsCachedCollection.js rename {packages/rocketchat-ui-admin => app/ui-admin}/client/admin.html (86%) create mode 100644 app/ui-admin/client/admin.js rename {packages/rocketchat-ui-admin => app/ui-admin}/client/adminFlex.html (89%) create mode 100644 app/ui-admin/client/adminFlex.js rename {packages/rocketchat-ui-admin => app/ui-admin}/client/adminInfo.html (78%) rename {packages/rocketchat-ui-admin => app/ui-admin}/client/adminInfo.js (80%) create mode 100644 app/ui-admin/client/index.js rename {packages/rocketchat-ui-admin => app/ui-admin}/client/rooms/adminRoomInfo.html (94%) rename {packages/rocketchat-ui-admin => app/ui-admin}/client/rooms/adminRoomInfo.js (76%) create mode 100644 app/ui-admin/client/rooms/adminRooms.html create mode 100644 app/ui-admin/client/rooms/adminRooms.js rename {packages/rocketchat-ui-admin => app/ui-admin}/client/rooms/channelSettingsDefault.html (100%) rename {packages/rocketchat-ui-admin => app/ui-admin}/client/rooms/channelSettingsDefault.js (80%) rename {packages/rocketchat-ui-admin => app/ui-admin}/client/users/adminInviteUser.html (100%) rename {packages/rocketchat-ui-admin => app/ui-admin}/client/users/adminInviteUser.js (79%) rename {packages/rocketchat-ui-admin => app/ui-admin}/client/users/adminUserChannels.html (100%) create mode 100644 app/ui-admin/client/users/adminUserChannels.js rename {packages/rocketchat-ui-admin => app/ui-admin}/client/users/adminUserEdit.html (100%) create mode 100644 app/ui-admin/client/users/adminUserInfo.html create mode 100644 app/ui-admin/client/users/adminUsers.html create mode 100644 app/ui-admin/client/users/adminUsers.js create mode 100644 app/ui-admin/server/index.js create mode 100644 app/ui-admin/server/publications/adminRooms.js create mode 100644 app/ui-cached-collection/client/index.js create mode 100644 app/ui-cached-collection/client/models/CachedCollection.js create mode 100644 app/ui-cached-collection/index.js rename {packages/rocketchat-ui-clean-history => app/ui-clean-history}/README.md (100%) create mode 100644 app/ui-clean-history/client/index.js create mode 100644 app/ui-clean-history/client/lib/startup.js rename {packages/rocketchat-ui-clean-history => app/ui-clean-history}/client/views/cleanHistory.html (91%) rename {packages/rocketchat-ui-clean-history => app/ui-clean-history}/client/views/cleanHistory.js (89%) rename {packages/rocketchat-ui-clean-history => app/ui-clean-history}/client/views/stylesheets/cleanHistory.css (80%) create mode 100644 app/ui-clean-history/index.js rename {packages/rocketchat-ui-flextab => app/ui-flextab}/README.md (100%) rename {packages/rocketchat-ui-flextab => app/ui-flextab}/client/flexTabBar.html (85%) create mode 100644 app/ui-flextab/client/flexTabBar.js create mode 100644 app/ui-flextab/client/index.js rename {packages/rocketchat-ui-flextab => app/ui-flextab}/client/tabs/inviteUsers.html (86%) rename {packages/rocketchat-ui-flextab => app/ui-flextab}/client/tabs/inviteUsers.js (78%) rename {packages/rocketchat-ui-flextab => app/ui-flextab}/client/tabs/keyboardShortcuts.html (88%) create mode 100644 app/ui-flextab/client/tabs/membersList.html create mode 100644 app/ui-flextab/client/tabs/membersList.js rename {packages/rocketchat-ui-flextab => app/ui-flextab}/client/tabs/uploadedFilesList.html (84%) create mode 100644 app/ui-flextab/client/tabs/uploadedFilesList.js create mode 100644 app/ui-flextab/client/tabs/userActions.js create mode 100644 app/ui-flextab/client/tabs/userEdit.html create mode 100644 app/ui-flextab/client/tabs/userEdit.js rename {packages/rocketchat-ui-flextab => app/ui-flextab}/client/tabs/userInfo.html (81%) create mode 100644 app/ui-flextab/client/tabs/userInfo.js create mode 100644 app/ui-flextab/index.js rename {packages/rocketchat-ui-login => app/ui-login}/README.md (100%) create mode 100644 app/ui-login/client/index.js rename {packages/rocketchat-ui-login => app/ui-login}/client/login/footer.html (100%) rename {packages/rocketchat-ui-login => app/ui-login}/client/login/footer.js (80%) create mode 100644 app/ui-login/client/login/form.html create mode 100644 app/ui-login/client/login/form.js rename {packages/rocketchat-ui-login => app/ui-login}/client/login/header.html (100%) create mode 100644 app/ui-login/client/login/header.js rename {packages/rocketchat-ui-login => app/ui-login}/client/login/layout.html (100%) create mode 100644 app/ui-login/client/login/layout.js rename {packages/rocketchat-ui-login => app/ui-login}/client/login/services.html (100%) create mode 100644 app/ui-login/client/login/services.js rename {packages/rocketchat-ui-login => app/ui-login}/client/login/social.html (100%) rename {packages/rocketchat-ui-login => app/ui-login}/client/login/social.js (100%) rename {packages/rocketchat-ui-login => app/ui-login}/client/reset-password/resetPassword.html (100%) rename {packages/rocketchat-ui-login => app/ui-login}/client/reset-password/resetPassword.js (77%) create mode 100644 app/ui-login/client/routes.js rename {packages/rocketchat-ui-login => app/ui-login}/client/username/layout.html (100%) create mode 100644 app/ui-login/client/username/layout.js rename {packages/rocketchat-ui-login => app/ui-login}/client/username/username.html (82%) rename {packages/rocketchat-ui-login => app/ui-login}/client/username/username.js (76%) create mode 100644 app/ui-login/index.js create mode 100644 app/ui-master/.eslintrc rename {packages/rocketchat-ui-master => app/ui-master}/README.md (100%) rename {packages/rocketchat-ui-master => app/ui-master}/client/error.html (100%) create mode 100644 app/ui-master/client/index.js create mode 100644 app/ui-master/client/loading/index.js create mode 100644 app/ui-master/client/loading/loading.css rename {packages/rocketchat-ui-master/client => app/ui-master/client/loading}/loading.html (100%) rename {packages/rocketchat-ui-master => app/ui-master}/client/logoLayout.html (100%) create mode 100644 app/ui-master/client/main.html create mode 100644 app/ui-master/client/main.js create mode 100644 app/ui-master/public/README.md create mode 100644 app/ui-master/public/generateHTML.js create mode 100644 app/ui-master/public/generateSprite.js create mode 100644 app/ui-master/public/icons/Bell-off.svg create mode 100644 app/ui-master/public/icons/Download.svg create mode 100644 app/ui-master/public/icons/Eye.svg create mode 100644 app/ui-master/public/icons/File-google-drive.svg create mode 100644 app/ui-master/public/icons/File-keynote.svg create mode 100644 app/ui-master/public/icons/Files-audio.svg create mode 100644 app/ui-master/public/icons/Files-video.svg create mode 100644 app/ui-master/public/icons/Files-zip.svg create mode 100644 app/ui-master/public/icons/Multiline.svg create mode 100644 app/ui-master/public/icons/Send-active.svg create mode 100644 app/ui-master/public/icons/Star-filled.svg create mode 100644 app/ui-master/public/icons/Video-off.svg create mode 100644 app/ui-master/public/icons/Volume-disable.svg create mode 100644 app/ui-master/public/icons/add-reaction.svg create mode 100644 app/ui-master/public/icons/arrow-down.svg create mode 100644 app/ui-master/public/icons/at.svg create mode 100644 app/ui-master/public/icons/back.svg create mode 100644 app/ui-master/public/icons/ban.svg create mode 100644 app/ui-master/public/icons/bell.svg create mode 100644 app/ui-master/public/icons/bold.svg create mode 100644 app/ui-master/public/icons/book.svg create mode 100644 app/ui-master/public/icons/calendar.svg create mode 100644 app/ui-master/public/icons/card.svg create mode 100644 app/ui-master/public/icons/chat.svg create mode 100644 app/ui-master/public/icons/check.svg create mode 100644 app/ui-master/public/icons/checkmark-circled.svg create mode 100644 app/ui-master/public/icons/circle-cross.svg create mode 100644 app/ui-master/public/icons/circle.svg create mode 100644 app/ui-master/public/icons/circled-arrow-down.svg create mode 100644 app/ui-master/public/icons/clip.svg create mode 100644 app/ui-master/public/icons/clipboard.svg create mode 100644 app/ui-master/public/icons/clock.svg create mode 100644 app/ui-master/public/icons/cloud-plus.svg create mode 100644 app/ui-master/public/icons/code.svg create mode 100644 app/ui-master/public/icons/cog.svg create mode 100644 app/ui-master/public/icons/computer.svg create mode 100644 app/ui-master/public/icons/copy.svg create mode 100644 app/ui-master/public/icons/cross.svg create mode 100644 app/ui-master/public/icons/cube.svg create mode 100644 app/ui-master/public/icons/customize.svg create mode 100644 app/ui-master/public/icons/discover.svg create mode 100644 app/ui-master/public/icons/discussion.svg create mode 100644 app/ui-master/public/icons/edit-rounded.svg create mode 100644 app/ui-master/public/icons/edit.svg create mode 100644 app/ui-master/public/icons/emoji.svg create mode 100644 app/ui-master/public/icons/eraser.svg create mode 100644 app/ui-master/public/icons/eye-off.svg create mode 100644 app/ui-master/public/icons/facebook.svg create mode 100644 app/ui-master/public/icons/file-document.svg create mode 100644 app/ui-master/public/icons/file-generic.svg create mode 100644 app/ui-master/public/icons/file-pdf.svg create mode 100644 app/ui-master/public/icons/file-sheets.svg create mode 100644 app/ui-master/public/icons/flag.svg create mode 100644 app/ui-master/public/icons/folder.svg create mode 100644 app/ui-master/public/icons/github.svg create mode 100644 app/ui-master/public/icons/gitlab.svg create mode 100644 app/ui-master/public/icons/google.svg create mode 100644 app/ui-master/public/icons/hand-pointer.svg create mode 100644 app/ui-master/public/icons/hashtag.svg create mode 100644 app/ui-master/public/icons/help.svg create mode 100644 app/ui-master/public/icons/hubot.svg create mode 100644 app/ui-master/public/icons/import.svg create mode 100644 app/ui-master/public/icons/info-circled.svg create mode 100644 app/ui-master/public/icons/italic.svg create mode 100644 app/ui-master/public/icons/jump.svg create mode 100644 app/ui-master/public/icons/key.svg create mode 100644 app/ui-master/public/icons/keyboard.svg create mode 100644 app/ui-master/public/icons/language.svg create mode 100644 app/ui-master/public/icons/linkedin.svg create mode 100644 app/ui-master/public/icons/list-alt.svg create mode 100644 app/ui-master/public/icons/list.svg create mode 100644 app/ui-master/public/icons/livechat.svg create mode 100644 app/ui-master/public/icons/loading.svg create mode 100644 app/ui-master/public/icons/lock.svg create mode 100644 app/ui-master/public/icons/magnifier.svg create mode 100644 app/ui-master/public/icons/mail.svg create mode 100644 app/ui-master/public/icons/map-pin.svg create mode 100644 app/ui-master/public/icons/menu.svg create mode 100644 app/ui-master/public/icons/message.svg create mode 100644 app/ui-master/public/icons/mic.svg create mode 100644 app/ui-master/public/icons/mobile.svg create mode 100644 app/ui-master/public/icons/modal-warning.svg create mode 100644 app/ui-master/public/icons/mute.svg create mode 100644 app/ui-master/public/icons/pause.svg create mode 100644 app/ui-master/public/icons/permalink.svg create mode 100644 app/ui-master/public/icons/pin.svg create mode 100644 app/ui-master/public/icons/play-solid.svg create mode 100644 app/ui-master/public/icons/play.svg create mode 100644 app/ui-master/public/icons/plus.svg create mode 100644 app/ui-master/public/icons/podcast.svg create mode 100644 app/ui-master/public/icons/post.svg create mode 100644 app/ui-master/public/icons/queue.svg create mode 100644 app/ui-master/public/icons/quote.svg create mode 100644 app/ui-master/public/icons/reload.svg create mode 100644 app/ui-master/public/icons/reply-directly.svg create mode 100644 app/ui-master/public/icons/reply.svg create mode 100644 app/ui-master/public/icons/report.svg create mode 100644 app/ui-master/public/icons/send.svg create mode 100644 app/ui-master/public/icons/share.svg create mode 100644 app/ui-master/public/icons/shield-alt.svg create mode 100644 app/ui-master/public/icons/shield-check.svg create mode 100644 app/ui-master/public/icons/shield.svg create mode 100644 app/ui-master/public/icons/sign-out.svg create mode 100644 app/ui-master/public/icons/sort-amount-down.svg create mode 100644 app/ui-master/public/icons/sort-down.svg create mode 100644 app/ui-master/public/icons/sort-up.svg create mode 100644 app/ui-master/public/icons/sort.svg create mode 100644 app/ui-master/public/icons/star.svg create mode 100644 app/ui-master/public/icons/strike.svg create mode 100644 app/ui-master/public/icons/team.svg create mode 100644 app/ui-master/public/icons/th-list.svg create mode 100644 app/ui-master/public/icons/thread.svg create mode 100644 app/ui-master/public/icons/trash.svg create mode 100644 app/ui-master/public/icons/twitter.svg create mode 100644 app/ui-master/public/icons/upload.svg create mode 100644 app/ui-master/public/icons/user-plus.svg create mode 100644 app/ui-master/public/icons/user-rounded.svg create mode 100644 app/ui-master/public/icons/user.svg create mode 100644 app/ui-master/public/icons/video.svg create mode 100644 app/ui-master/public/icons/volume-mute.svg create mode 100644 app/ui-master/public/icons/volume.svg create mode 100644 app/ui-master/public/icons/warning.svg create mode 100644 app/ui-master/server/index.js create mode 100644 app/ui-master/server/inject.js rename {packages/rocketchat-ui-message => app/ui-message}/README.md (100%) create mode 100644 app/ui-message/client/index.js create mode 100644 app/ui-message/client/message.html create mode 100644 app/ui-message/client/message.js create mode 100644 app/ui-message/client/messageBox/messageBox.html create mode 100644 app/ui-message/client/messageBox/messageBox.js create mode 100644 app/ui-message/client/messageBox/messageBoxActions.js create mode 100644 app/ui-message/client/messageBox/messageBoxAudioMessage.html create mode 100644 app/ui-message/client/messageBox/messageBoxAudioMessage.js create mode 100644 app/ui-message/client/messageBox/messageBoxAutogrow.js create mode 100644 app/ui-message/client/messageBox/messageBoxFormatting.js create mode 100644 app/ui-message/client/messageBox/messageBoxNotSubscribed.html create mode 100644 app/ui-message/client/messageBox/messageBoxNotSubscribed.js create mode 100644 app/ui-message/client/messageBox/messageBoxReadOnly.html create mode 100644 app/ui-message/client/messageBox/messageBoxReadOnly.js create mode 100644 app/ui-message/client/messageBox/messageBoxReplyPreview.html create mode 100644 app/ui-message/client/messageBox/messageBoxReplyPreview.js create mode 100644 app/ui-message/client/messageBox/messageBoxTyping.html create mode 100644 app/ui-message/client/messageBox/messageBoxTyping.js create mode 100644 app/ui-message/client/messageThread.html rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopup.html (100%) rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopup.js (93%) rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopupChannel.html (100%) create mode 100644 app/ui-message/client/popup/messagePopupChannel.js create mode 100644 app/ui-message/client/popup/messagePopupConfig.html create mode 100644 app/ui-message/client/popup/messagePopupConfig.js rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopupEmoji.html (100%) create mode 100644 app/ui-message/client/popup/messagePopupEmoji.js rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopupSlashCommand.html (100%) rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopupSlashCommandPreview.html (100%) rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopupSlashCommandPreview.js (92%) rename {packages/rocketchat-ui-message => app/ui-message}/client/popup/messagePopupUser.html (100%) create mode 100644 app/ui-message/index.js rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/README.md (100%) rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/client/chatRoomItem.html (100%) create mode 100644 app/ui-sidenav/client/chatRoomItem.js create mode 100644 app/ui-sidenav/client/index.js rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/client/roomList.html (100%) create mode 100644 app/ui-sidenav/client/roomList.js create mode 100644 app/ui-sidenav/client/sideNav.html create mode 100644 app/ui-sidenav/client/sideNav.js rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/client/sidebarHeader.html (92%) create mode 100644 app/ui-sidenav/client/sidebarHeader.js create mode 100644 app/ui-sidenav/client/sidebarItem.html create mode 100644 app/ui-sidenav/client/sidebarItem.js rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/client/sortlist.html (84%) create mode 100644 app/ui-sidenav/client/sortlist.js rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/client/toolbar.html (93%) create mode 100644 app/ui-sidenav/client/toolbar.js rename {packages/rocketchat-ui-sidenav => app/ui-sidenav}/client/userStatus.html (100%) create mode 100644 app/ui-sidenav/index.js create mode 100644 app/ui-utils/client/config.js create mode 100644 app/ui-utils/client/index.js create mode 100644 app/ui-utils/client/lib/AccountBox.js rename {packages/rocketchat-lib/client => app/ui-utils/client/lib}/AdminBox.js (76%) create mode 100644 app/ui-utils/client/lib/ChannelActions.js create mode 100644 app/ui-utils/client/lib/IframeLogin.js create mode 100644 app/ui-utils/client/lib/Layout.js create mode 100644 app/ui-utils/client/lib/MessageAction.js rename {packages/rocketchat-lib => app/ui-utils}/client/lib/RocketChatTabBar.js (81%) create mode 100644 app/ui-utils/client/lib/RoomHistoryManager.js create mode 100644 app/ui-utils/client/lib/RoomManager.js create mode 100644 app/ui-utils/client/lib/SideNav.js create mode 100644 app/ui-utils/client/lib/TabBar.js rename {packages/rocketchat-ui/client/views/app => app/ui-utils/client/lib}/alerts.html (100%) rename {packages/rocketchat-ui/client/views/app => app/ui-utils/client/lib}/alerts.js (87%) create mode 100644 app/ui-utils/client/lib/avatar.js rename {packages/rocketchat-lib => app/ui-utils}/client/lib/callMethod.js (84%) create mode 100644 app/ui-utils/client/lib/fireGlobalEvent.js create mode 100644 app/ui-utils/client/lib/keyCodes.js create mode 100644 app/ui-utils/client/lib/mainReady.js create mode 100644 app/ui-utils/client/lib/menu.js create mode 100644 app/ui-utils/client/lib/messageArgs.js create mode 100644 app/ui-utils/client/lib/messageBox.js create mode 100644 app/ui-utils/client/lib/messageContext.js create mode 100644 app/ui-utils/client/lib/modal.html create mode 100644 app/ui-utils/client/lib/modal.js create mode 100644 app/ui-utils/client/lib/openRoom.js rename {packages/rocketchat-ui/client/views/app => app/ui-utils/client/lib}/popout.html (94%) rename {packages/rocketchat-ui/client/views/app => app/ui-utils/client/lib}/popout.js (93%) rename {packages/rocketchat-ui/client/views/app => app/ui-utils/client/lib}/popover.html (79%) rename {packages/rocketchat-ui/client/views/app => app/ui-utils/client/lib}/popover.js (79%) create mode 100644 app/ui-utils/client/lib/prependReplies.js rename {packages/rocketchat-ui => app/ui-utils}/client/lib/readMessages.js (86%) create mode 100644 app/ui-utils/client/lib/renderMessageBody.js create mode 100644 app/ui-utils/client/lib/rtl.js create mode 100644 app/ui-utils/index.js create mode 100644 app/ui-utils/lib/Message.js create mode 100644 app/ui-utils/lib/MessageProperties.js create mode 100644 app/ui-utils/lib/MessageTypes.js create mode 100644 app/ui-utils/server/index.js create mode 100644 app/ui-utils/tests/server.mocks.js create mode 100644 app/ui-utils/tests/server.tests.js rename {packages/rocketchat-ui-vrecord => app/ui-vrecord}/README.md (100%) create mode 100644 app/ui-vrecord/client/VRecDialog.js create mode 100644 app/ui-vrecord/client/index.js rename {packages/rocketchat-ui-vrecord => app/ui-vrecord}/client/vrecord.css (100%) rename {packages/rocketchat-ui-vrecord => app/ui-vrecord}/client/vrecord.html (100%) create mode 100644 app/ui-vrecord/client/vrecord.js create mode 100644 app/ui-vrecord/server/index.js create mode 100644 app/ui-vrecord/server/settings.js rename {packages/rocketchat-ui => app/ui}/README.md (100%) create mode 100644 app/ui/client/components/contextualBar.html create mode 100644 app/ui/client/components/contextualBar.js create mode 100644 app/ui/client/components/header/header.html create mode 100644 app/ui/client/components/header/header.js create mode 100644 app/ui/client/components/header/headerRoom.html create mode 100644 app/ui/client/components/header/headerRoom.js rename {packages/rocketchat-ui => app/ui}/client/components/icon.html (100%) create mode 100644 app/ui/client/components/icon.js create mode 100644 app/ui/client/components/popupList.html create mode 100644 app/ui/client/components/popupList.js create mode 100644 app/ui/client/components/selectDropdown.html create mode 100644 app/ui/client/components/status.html create mode 100644 app/ui/client/components/status.js rename {packages/rocketchat-ui => app/ui}/client/components/table.html (100%) rename {packages/rocketchat-ui => app/ui}/client/components/table.js (93%) create mode 100644 app/ui/client/components/tabs.html create mode 100644 app/ui/client/components/tabs.js create mode 100644 app/ui/client/index.js rename {packages/rocketchat-ui => app/ui}/client/lib/Modernizr.js (100%) create mode 100644 app/ui/client/lib/accounts.js create mode 100644 app/ui/client/lib/chatMessages.js rename {packages/rocketchat-ui => app/ui}/client/lib/codeMirror/codeMirror.js (100%) rename {packages/rocketchat-ui => app/ui}/client/lib/codeMirror/codeMirrorComponent.html (100%) rename {packages/rocketchat-ui => app/ui}/client/lib/codeMirror/codeMirrorComponent.js (88%) create mode 100644 app/ui/client/lib/collections.js rename {packages/rocketchat-ui => app/ui}/client/lib/customEventPolyfill.js (100%) rename {packages/rocketchat-ui => app/ui}/client/lib/fileUpload.js (81%) rename {packages/rocketchat-ui => app/ui}/client/lib/iframeCommands.js (81%) create mode 100644 app/ui/client/lib/menu.js create mode 100644 app/ui/client/lib/msgTyping.js create mode 100644 app/ui/client/lib/notification.js rename {packages/rocketchat-ui => app/ui}/client/lib/parentTemplate.js (79%) create mode 100644 app/ui/client/lib/recorderjs/audioEncoder.js create mode 100644 app/ui/client/lib/recorderjs/audioRecorder.js create mode 100644 app/ui/client/lib/recorderjs/videoRecorder.js rename {packages/rocketchat-ui => app/ui}/client/lib/rocket.js (81%) create mode 100644 app/ui/client/lib/textarea-cursor.js rename {packages/rocketchat-ui => app/ui}/client/views/404/invalidSecretURL.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/404/roomNotFound.html (100%) create mode 100644 app/ui/client/views/404/roomNotFound.js rename {packages/rocketchat-ui => app/ui}/client/views/app/audioNotification.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/burger.html (100%) create mode 100644 app/ui/client/views/app/burger.js rename {packages/rocketchat-ui => app/ui}/client/views/app/createChannel.html (91%) create mode 100644 app/ui/client/views/app/createChannel.js create mode 100644 app/ui/client/views/app/directory.css create mode 100644 app/ui/client/views/app/directory.html create mode 100644 app/ui/client/views/app/directory.js create mode 100644 app/ui/client/views/app/editStatus.css create mode 100644 app/ui/client/views/app/editStatus.html create mode 100644 app/ui/client/views/app/editStatus.js rename {packages/rocketchat-ui => app/ui}/client/views/app/fullModal.html (100%) create mode 100644 app/ui/client/views/app/fullModal.js create mode 100644 app/ui/client/views/app/helpers.js rename {packages/rocketchat-ui => app/ui}/client/views/app/home.html (100%) create mode 100644 app/ui/client/views/app/home.js rename {packages/rocketchat-ui => app/ui}/client/views/app/notAuthorized.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/pageContainer.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/pageSettingsContainer.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/photoswipe.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/photoswipe.js (92%) create mode 100644 app/ui/client/views/app/room.html create mode 100644 app/ui/client/views/app/room.js rename {packages/rocketchat-ui => app/ui}/client/views/app/roomSearch.html (100%) create mode 100644 app/ui/client/views/app/roomSearch.js rename {packages/rocketchat-ui => app/ui}/client/views/app/secretURL.html (100%) create mode 100644 app/ui/client/views/app/secretURL.js rename {packages/rocketchat-ui => app/ui}/client/views/app/tests/helpers.tests.js (99%) rename {packages/rocketchat-ui => app/ui}/client/views/app/userSearch.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/videoCall/videoButtons.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/app/videoCall/videoButtons.js (87%) create mode 100644 app/ui/client/views/app/videoCall/videoCall.html create mode 100644 app/ui/client/views/app/videoCall/videoCall.js create mode 100644 app/ui/client/views/cmsPage.html create mode 100644 app/ui/client/views/cmsPage.js rename {packages/rocketchat-ui => app/ui}/client/views/fxos.html (100%) rename {packages/rocketchat-ui => app/ui}/client/views/fxos.js (85%) rename {packages/rocketchat-ui => app/ui}/client/views/modal.html (100%) create mode 100644 app/ui/client/views/modal.js create mode 100644 app/ui/index.js create mode 100644 app/user-data-download/index.js rename {packages/rocketchat-user-data-download => app/user-data-download}/server/cronProcessDownloads.js (80%) create mode 100644 app/user-data-download/server/index.js create mode 100644 app/user-data-download/server/startup/settings.js create mode 100644 app/user-status/client/admin/adminUserStatus.html create mode 100644 app/user-status/client/admin/adminUserStatus.js create mode 100644 app/user-status/client/admin/adminUserStatusEdit.html create mode 100644 app/user-status/client/admin/adminUserStatusInfo.html create mode 100644 app/user-status/client/admin/route.js create mode 100644 app/user-status/client/admin/startup.js create mode 100644 app/user-status/client/admin/userStatusEdit.html create mode 100644 app/user-status/client/admin/userStatusEdit.js create mode 100644 app/user-status/client/admin/userStatusInfo.html create mode 100644 app/user-status/client/admin/userStatusInfo.js create mode 100644 app/user-status/client/admin/userStatusPreview.html create mode 100644 app/user-status/client/index.js create mode 100644 app/user-status/client/lib/customUserStatus.js create mode 100644 app/user-status/client/lib/userStatus.js create mode 100644 app/user-status/client/notifications/deleteCustomUserStatus.js create mode 100644 app/user-status/client/notifications/updateCustomUserStatus.js create mode 100644 app/user-status/index.js create mode 100644 app/user-status/server/index.js create mode 100644 app/user-status/server/methods/deleteCustomUserStatus.js create mode 100644 app/user-status/server/methods/getUserStatusText.js create mode 100644 app/user-status/server/methods/insertOrUpdateUserStatus.js create mode 100644 app/user-status/server/methods/listCustomUserStatus.js create mode 100644 app/user-status/server/methods/setUserStatus.js create mode 100644 app/user-status/server/publications/fullUserStatusData.js create mode 100644 app/utils/client/index.js create mode 100644 app/utils/client/lib/CustomTranslations.js create mode 100644 app/utils/client/lib/RestApiClient.js create mode 100644 app/utils/client/lib/canDeleteMessage.js create mode 100644 app/utils/client/lib/handleError.js create mode 100644 app/utils/client/lib/roomTypes.js create mode 100644 app/utils/index.js rename {packages/rocketchat-lib => app/utils}/lib/RoomTypeConfig.js (78%) rename {packages/rocketchat-lib => app/utils}/lib/RoomTypesCommon.js (77%) create mode 100644 app/utils/lib/fileUploadRestrictions.js create mode 100644 app/utils/lib/getAvatarColor.js create mode 100644 app/utils/lib/getAvatarURL.js rename {packages/rocketchat-lib => app/utils}/lib/getDefaultSubscriptionPref.js (90%) create mode 100644 app/utils/lib/getRoomAvatarURL.js create mode 100644 app/utils/lib/getURL.js create mode 100644 app/utils/lib/getUserAvatarURL.js create mode 100644 app/utils/lib/getUserNotificationPreference.js create mode 100644 app/utils/lib/getUserPreference.js create mode 100644 app/utils/lib/getValidRoomName.js create mode 100644 app/utils/lib/isEmail.js create mode 100644 app/utils/lib/isURL.js create mode 100644 app/utils/lib/mimeTypes.js create mode 100644 app/utils/lib/placeholders.js create mode 100644 app/utils/lib/roomExit.js create mode 100644 app/utils/lib/slashCommand.js create mode 100644 app/utils/lib/tapi18n.js create mode 100644 app/utils/lib/templateVarHandler.js create mode 100644 app/utils/rocketchat.info create mode 100644 app/utils/server/functions/getDefaultUserFields.js create mode 100644 app/utils/server/functions/getMongoInfo.js create mode 100644 app/utils/server/functions/isDocker.js create mode 100644 app/utils/server/index.js create mode 100644 app/utils/server/lib/normalizeMessagesForUser.js create mode 100644 app/utils/server/lib/roomTypes.js create mode 100644 app/version-check/client/index.js create mode 100644 app/version-check/server/addSettings.js create mode 100644 app/version-check/server/functions/checkVersionUpdate.js create mode 100644 app/version-check/server/functions/getNewUpdates.js create mode 100644 app/version-check/server/index.js create mode 100644 app/version-check/server/logger.js create mode 100644 app/version-check/server/methods/banner_dismiss.js create mode 100644 app/version-check/server/sampleUpdateData.js create mode 100644 app/videobridge/.eslintrc create mode 100644 app/videobridge/client/actionLink.js create mode 100644 app/videobridge/client/index.js rename {packages/rocketchat-videobridge => app/videobridge}/client/stylesheets/video.less (100%) create mode 100644 app/videobridge/client/tabBar.js rename {packages/rocketchat-videobridge => app/videobridge}/client/views/bbbLiveView.html (100%) rename {packages/rocketchat-videobridge => app/videobridge}/client/views/videoFlexTab.html (100%) create mode 100644 app/videobridge/client/views/videoFlexTab.js rename {packages/rocketchat-videobridge => app/videobridge}/client/views/videoFlexTabBbb.html (100%) create mode 100644 app/videobridge/client/views/videoFlexTabBbb.js create mode 100644 app/videobridge/constants.js create mode 100644 app/videobridge/lib/messageType.js create mode 100644 app/videobridge/server/actionLink.js create mode 100644 app/videobridge/server/index.js rename {packages/rocketchat-videobridge => app/videobridge}/server/methods/bbb.js (79%) create mode 100644 app/videobridge/server/methods/jitsiSetTimeout.js create mode 100644 app/videobridge/server/settings.js create mode 100644 app/webdav/README.md create mode 100644 app/webdav/client/actionButton.js create mode 100644 app/webdav/client/addWebdavAccount.html create mode 100644 app/webdav/client/addWebdavAccount.js create mode 100644 app/webdav/client/index.js create mode 100644 app/webdav/client/selectWebdavAccount.html create mode 100644 app/webdav/client/selectWebdavAccount.js create mode 100644 app/webdav/client/startup/messageBoxActions.js create mode 100644 app/webdav/client/startup/subscription.js create mode 100644 app/webdav/client/webdavFilePicker.css create mode 100644 app/webdav/client/webdavFilePicker.html create mode 100644 app/webdav/client/webdavFilePicker.js create mode 100644 app/webdav/server/index.js create mode 100644 app/webdav/server/methods/addWebdavAccount.js create mode 100644 app/webdav/server/methods/getFileFromWebdav.js create mode 100644 app/webdav/server/methods/getWebdavFileList.js create mode 100644 app/webdav/server/methods/removeWebdavAccount.js create mode 100644 app/webdav/server/methods/uploadFileToWebdav.js create mode 100644 app/webdav/server/publications/webdavAccounts.js create mode 100644 app/webdav/server/startup/settings.js rename {packages/rocketchat-webrtc => app/webrtc}/client/WebRTCClass.js (83%) rename {packages/rocketchat-webrtc => app/webrtc}/client/adapter.js (100%) create mode 100644 app/webrtc/client/index.js rename {packages/rocketchat-webrtc => app/webrtc}/client/screenShare.js (89%) create mode 100644 app/webrtc/index.js create mode 100644 app/webrtc/server/index.js create mode 100644 app/webrtc/server/settings.js create mode 100644 app/wordpress/client/index.js rename {packages/rocketchat-wordpress => app/wordpress}/client/wordpress-login-button.css (100%) create mode 100644 app/wordpress/lib/common.js create mode 100644 app/wordpress/server/index.js create mode 100644 app/wordpress/server/startup.js create mode 100644 client/head.html create mode 100644 client/importPackages.js create mode 100644 client/importsCss.js delete mode 100644 client/lib/handleError.js create mode 100644 client/routes/pageNotFound.js create mode 100644 client/routes/stylesheets/pageNotFound.css create mode 120000 imports/client/limax create mode 120000 imports/client/map-age-cleaner create mode 120000 imports/client/mem create mode 120000 imports/client/mimic-fn create mode 120000 imports/client/p-defer create mode 120000 imports/client/p-is-promise create mode 120000 imports/client/pinyin delete mode 100644 imports/message-read-receipt/server/dbIndexes.js delete mode 100644 imports/message-read-receipt/server/models/ReadReceipts.js delete mode 100644 imports/personal-access-tokens/server/models/Users.js delete mode 100644 imports/personal-access-tokens/server/models/index.js delete mode 100644 imports/personal-access-tokens/server/settings.js create mode 100644 imports/startup/client/listenActiveUsers.js create mode 100644 imports/users-presence/server/activeUsers.js create mode 100644 imports/users-presence/server/index.js delete mode 100644 lib/francocatena_fix.js create mode 100644 packages/.eslintrc delete mode 100644 packages/autoupdate/QA.md delete mode 100644 packages/autoupdate/README.md delete mode 100644 packages/autoupdate/autoupdate_client.js delete mode 100644 packages/autoupdate/autoupdate_cordova.js delete mode 100644 packages/autoupdate/autoupdate_server.js delete mode 100644 packages/autoupdate/package.js delete mode 100644 packages/chatpal-search/client/route.js delete mode 100644 packages/chatpal-search/client/template/admin.js delete mode 100644 packages/chatpal-search/client/template/result.html delete mode 100644 packages/chatpal-search/client/template/result.js delete mode 100644 packages/chatpal-search/package.js delete mode 100644 packages/chatpal-search/server/asset/config.js delete mode 100644 packages/chatpal-search/server/provider/index.js delete mode 100644 packages/chatpal-search/server/provider/provider.js delete mode 100644 packages/chatpal-search/server/utils/logger.js delete mode 100644 packages/chatpal-search/server/utils/utils.js delete mode 100644 packages/meteor-accounts-saml/package.js delete mode 100644 packages/meteor-accounts-saml/saml_client.js delete mode 100644 packages/meteor-accounts-saml/saml_rocketchat.js delete mode 100644 packages/meteor-accounts-saml/saml_server.js delete mode 100644 packages/meteor-accounts-saml/saml_utils.js create mode 100644 packages/meteor-autocomplete/client/index.js create mode 100644 packages/meteor-autocomplete/server/index.js create mode 100644 packages/meteor-timesync/client/index.js create mode 100644 packages/meteor-timesync/server/index.js delete mode 100644 packages/rocketchat-2fa/client/accountSecurity.html delete mode 100644 packages/rocketchat-2fa/package.js delete mode 100644 packages/rocketchat-2fa/server/loginHandler.js delete mode 100644 packages/rocketchat-2fa/server/methods/disable.js delete mode 100644 packages/rocketchat-2fa/server/methods/enable.js delete mode 100644 packages/rocketchat-2fa/server/methods/regenerateCodes.js delete mode 100644 packages/rocketchat-2fa/server/methods/validateTempToken.js delete mode 100644 packages/rocketchat-2fa/server/models/users.js delete mode 100644 packages/rocketchat-2fa/server/startup/settings.js delete mode 100644 packages/rocketchat-accounts/.npm/package/npm-shrinkwrap.json delete mode 100644 packages/rocketchat-accounts/package.js delete mode 100644 packages/rocketchat-action-links/both/lib/actionLinks.js delete mode 100644 packages/rocketchat-action-links/client/init.js delete mode 100644 packages/rocketchat-action-links/client/lib/actionLinks.js delete mode 100644 packages/rocketchat-action-links/package.js delete mode 100644 packages/rocketchat-action-links/server/actionLinkHandler.js delete mode 100644 packages/rocketchat-analytics/client/trackEvents.js delete mode 100644 packages/rocketchat-analytics/package.js delete mode 100644 packages/rocketchat-analytics/server/settings.js delete mode 100644 packages/rocketchat-api/package.js delete mode 100644 packages/rocketchat-api/server/api.js delete mode 100644 packages/rocketchat-api/server/default/info.js delete mode 100644 packages/rocketchat-api/server/helpers/deprecationWarning.js delete mode 100644 packages/rocketchat-api/server/helpers/getLoggedInUser.js delete mode 100644 packages/rocketchat-api/server/helpers/getPaginationItems.js delete mode 100644 packages/rocketchat-api/server/helpers/getUserFromParams.js delete mode 100644 packages/rocketchat-api/server/helpers/getUserInfo.js delete mode 100644 packages/rocketchat-api/server/helpers/insertUserObject.js delete mode 100644 packages/rocketchat-api/server/helpers/isUserFromParams.js delete mode 100644 packages/rocketchat-api/server/helpers/parseJsonQuery.js delete mode 100644 packages/rocketchat-api/server/helpers/requestParams.js delete mode 100644 packages/rocketchat-api/server/settings.js delete mode 100644 packages/rocketchat-api/server/v1/assets.js delete mode 100644 packages/rocketchat-api/server/v1/channels.js delete mode 100644 packages/rocketchat-api/server/v1/chat.js delete mode 100644 packages/rocketchat-api/server/v1/commands.js delete mode 100644 packages/rocketchat-api/server/v1/e2e.js delete mode 100644 packages/rocketchat-api/server/v1/emoji-custom.js delete mode 100644 packages/rocketchat-api/server/v1/groups.js delete mode 100644 packages/rocketchat-api/server/v1/im.js delete mode 100644 packages/rocketchat-api/server/v1/integrations.js delete mode 100644 packages/rocketchat-api/server/v1/misc.js delete mode 100644 packages/rocketchat-api/server/v1/permissions.js delete mode 100644 packages/rocketchat-api/server/v1/push.js delete mode 100644 packages/rocketchat-api/server/v1/roles.js delete mode 100644 packages/rocketchat-api/server/v1/rooms.js delete mode 100644 packages/rocketchat-api/server/v1/settings.js delete mode 100644 packages/rocketchat-api/server/v1/stats.js delete mode 100644 packages/rocketchat-api/server/v1/subscriptions.js delete mode 100644 packages/rocketchat-api/server/v1/users.js delete mode 100644 packages/rocketchat-apps/assets/stylesheets/apps.css delete mode 100644 packages/rocketchat-apps/client/admin/appInstall.html delete mode 100644 packages/rocketchat-apps/client/admin/appLogs.html delete mode 100644 packages/rocketchat-apps/client/admin/appLogs.js delete mode 100644 packages/rocketchat-apps/client/admin/appManage.html delete mode 100644 packages/rocketchat-apps/client/admin/appManage.js delete mode 100644 packages/rocketchat-apps/client/admin/appWhatIsIt.js delete mode 100644 packages/rocketchat-apps/client/admin/apps.html delete mode 100644 packages/rocketchat-apps/client/admin/apps.js delete mode 100644 packages/rocketchat-apps/client/communication/index.js delete mode 100644 packages/rocketchat-apps/client/communication/websockets.js delete mode 100644 packages/rocketchat-apps/client/orchestrator.js delete mode 100644 packages/rocketchat-apps/lib/Apps.js delete mode 100644 packages/rocketchat-apps/lib/misc/Utilities.js delete mode 100644 packages/rocketchat-apps/package.js delete mode 100644 packages/rocketchat-apps/server/bridges/commands.js delete mode 100644 packages/rocketchat-apps/server/bridges/environmental.js delete mode 100644 packages/rocketchat-apps/server/bridges/http.js delete mode 100644 packages/rocketchat-apps/server/bridges/index.js delete mode 100644 packages/rocketchat-apps/server/bridges/listeners.js delete mode 100644 packages/rocketchat-apps/server/bridges/messages.js delete mode 100644 packages/rocketchat-apps/server/bridges/persistence.js delete mode 100644 packages/rocketchat-apps/server/bridges/rooms.js delete mode 100644 packages/rocketchat-apps/server/bridges/settings.js delete mode 100644 packages/rocketchat-apps/server/bridges/users.js delete mode 100644 packages/rocketchat-apps/server/communication/methods.js delete mode 100644 packages/rocketchat-apps/server/communication/rest.js delete mode 100644 packages/rocketchat-apps/server/communication/websockets.js delete mode 100644 packages/rocketchat-apps/server/converters/messages.js delete mode 100644 packages/rocketchat-apps/server/converters/rooms.js delete mode 100644 packages/rocketchat-apps/server/converters/settings.js delete mode 100644 packages/rocketchat-apps/server/converters/users.js delete mode 100644 packages/rocketchat-apps/server/orchestrator.js delete mode 100644 packages/rocketchat-apps/server/storage/apps-logs-model.js delete mode 100644 packages/rocketchat-apps/server/storage/apps-model.js delete mode 100644 packages/rocketchat-apps/server/storage/apps-persistence-model.js delete mode 100644 packages/rocketchat-apps/server/storage/index.js delete mode 100644 packages/rocketchat-assets/package.js delete mode 100644 packages/rocketchat-assets/server/assets.js delete mode 100644 packages/rocketchat-authorization/client/hasPermission.js delete mode 100644 packages/rocketchat-authorization/client/hasRole.js delete mode 100644 packages/rocketchat-authorization/client/lib/ChatPermissions.js delete mode 100644 packages/rocketchat-authorization/client/lib/models/Roles.js delete mode 100644 packages/rocketchat-authorization/client/lib/models/Subscriptions.js delete mode 100644 packages/rocketchat-authorization/client/lib/models/Users.js delete mode 100644 packages/rocketchat-authorization/client/route.js delete mode 100644 packages/rocketchat-authorization/client/startup.js delete mode 100644 packages/rocketchat-authorization/client/stylesheets/permissions.css delete mode 100644 packages/rocketchat-authorization/client/usersNameChanged.js delete mode 100644 packages/rocketchat-authorization/client/views/permissions.html delete mode 100644 packages/rocketchat-authorization/client/views/permissions.js delete mode 100644 packages/rocketchat-authorization/client/views/permissionsRole.html delete mode 100644 packages/rocketchat-authorization/client/views/permissionsRole.js delete mode 100644 packages/rocketchat-authorization/lib/rocketchat.js delete mode 100644 packages/rocketchat-authorization/package.js delete mode 100644 packages/rocketchat-authorization/server/functions/addUserRoles.js delete mode 100644 packages/rocketchat-authorization/server/functions/canAccessRoom.js delete mode 100644 packages/rocketchat-authorization/server/functions/getRoles.js delete mode 100644 packages/rocketchat-authorization/server/functions/getUsersInRole.js delete mode 100644 packages/rocketchat-authorization/server/functions/hasPermission.js delete mode 100644 packages/rocketchat-authorization/server/functions/hasRole.js delete mode 100644 packages/rocketchat-authorization/server/functions/removeUserFromRoles.js delete mode 100644 packages/rocketchat-authorization/server/methods/addPermissionToRole.js delete mode 100644 packages/rocketchat-authorization/server/methods/addUserToRole.js delete mode 100644 packages/rocketchat-authorization/server/methods/deleteRole.js delete mode 100644 packages/rocketchat-authorization/server/methods/removeRoleFromPermission.js delete mode 100644 packages/rocketchat-authorization/server/methods/removeUserFromRole.js delete mode 100644 packages/rocketchat-authorization/server/methods/saveRole.js delete mode 100644 packages/rocketchat-authorization/server/models/Base.js delete mode 100644 packages/rocketchat-authorization/server/models/Permissions.js delete mode 100644 packages/rocketchat-authorization/server/models/Roles.js delete mode 100644 packages/rocketchat-authorization/server/models/Subscriptions.js delete mode 100644 packages/rocketchat-authorization/server/models/Users.js delete mode 100644 packages/rocketchat-authorization/server/publications/permissions.js delete mode 100644 packages/rocketchat-authorization/server/publications/roles.js delete mode 100644 packages/rocketchat-authorization/server/publications/usersInRole.js delete mode 100644 packages/rocketchat-authorization/server/startup.js delete mode 100644 packages/rocketchat-autolinker/client/client.js delete mode 100644 packages/rocketchat-autolinker/package.js delete mode 100644 packages/rocketchat-autolinker/server/settings.js delete mode 100644 packages/rocketchat-autotranslate/client/lib/actionButton.js delete mode 100644 packages/rocketchat-autotranslate/client/lib/autotranslate.js delete mode 100644 packages/rocketchat-autotranslate/client/lib/tabBar.js delete mode 100644 packages/rocketchat-autotranslate/package.js delete mode 100644 packages/rocketchat-autotranslate/server/autotranslate.js delete mode 100644 packages/rocketchat-autotranslate/server/methods/getSupportedLanguages.js delete mode 100644 packages/rocketchat-autotranslate/server/methods/saveSettings.js delete mode 100644 packages/rocketchat-autotranslate/server/methods/translateMessage.js delete mode 100644 packages/rocketchat-autotranslate/server/models/Messages.js delete mode 100644 packages/rocketchat-autotranslate/server/models/Subscriptions.js delete mode 100644 packages/rocketchat-autotranslate/server/permissions.js delete mode 100644 packages/rocketchat-autotranslate/server/settings.js delete mode 100644 packages/rocketchat-bigbluebutton/package.js delete mode 100644 packages/rocketchat-blockstack/client/main.js delete mode 100644 packages/rocketchat-blockstack/client/routes.js delete mode 100644 packages/rocketchat-blockstack/package.js delete mode 100644 packages/rocketchat-blockstack/server/logger.js delete mode 100644 packages/rocketchat-blockstack/server/loginHandler.js delete mode 100644 packages/rocketchat-blockstack/server/routes.js delete mode 100644 packages/rocketchat-blockstack/server/settings.js delete mode 100644 packages/rocketchat-bot-helpers/package.js delete mode 100644 packages/rocketchat-bot-helpers/server/index.js delete mode 100644 packages/rocketchat-bot-helpers/server/settings.js delete mode 100644 packages/rocketchat-cas/client/cas_client.js delete mode 100644 packages/rocketchat-cas/package.js delete mode 100644 packages/rocketchat-cas/server/cas_rocketchat.js delete mode 100644 packages/rocketchat-cas/server/cas_server.js delete mode 100644 packages/rocketchat-channel-settings-mail-messages/client/lib/startup.js delete mode 100644 packages/rocketchat-channel-settings-mail-messages/package.js delete mode 100644 packages/rocketchat-channel-settings-mail-messages/server/lib/startup.js delete mode 100644 packages/rocketchat-channel-settings-mail-messages/server/methods/mailMessages.js delete mode 100644 packages/rocketchat-channel-settings/client/startup/messageTypes.js delete mode 100644 packages/rocketchat-channel-settings/client/startup/tabBar.js delete mode 100644 packages/rocketchat-channel-settings/client/startup/trackSettingsChange.js delete mode 100644 packages/rocketchat-channel-settings/client/views/channelSettings.js delete mode 100644 packages/rocketchat-channel-settings/package.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveReactWhenReadOnly.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomAnnouncement.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomCustomFields.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomDescription.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomName.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomReadOnly.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomSystemMessages.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomTopic.js delete mode 100644 packages/rocketchat-channel-settings/server/functions/saveRoomType.js delete mode 100644 packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js delete mode 100644 packages/rocketchat-channel-settings/server/models/Messages.js delete mode 100644 packages/rocketchat-channel-settings/server/models/Rooms.js delete mode 100644 packages/rocketchat-channel-settings/server/startup.js delete mode 100644 packages/rocketchat-colors/client/client.js delete mode 100644 packages/rocketchat-colors/package.js delete mode 100644 packages/rocketchat-colors/server/settings.js delete mode 100644 packages/rocketchat-cors/common.js delete mode 100644 packages/rocketchat-cors/cors.js delete mode 100644 packages/rocketchat-cors/package.js delete mode 100644 packages/rocketchat-crowd/client/loginHelper.js delete mode 100644 packages/rocketchat-crowd/package.js delete mode 100644 packages/rocketchat-crowd/server/crowd.js delete mode 100644 packages/rocketchat-crowd/server/settings.js delete mode 100644 packages/rocketchat-custom-oauth/package.js delete mode 100644 packages/rocketchat-custom-oauth/server/custom_oauth_server.js delete mode 100644 packages/rocketchat-custom-sounds/client/admin/adminSounds.html delete mode 100644 packages/rocketchat-custom-sounds/client/admin/adminSounds.js delete mode 100644 packages/rocketchat-custom-sounds/client/admin/route.js delete mode 100644 packages/rocketchat-custom-sounds/client/admin/startup.js delete mode 100644 packages/rocketchat-custom-sounds/client/lib/CustomSounds.js delete mode 100644 packages/rocketchat-custom-sounds/client/models/CustomSounds.js delete mode 100644 packages/rocketchat-custom-sounds/client/notifications/deleteCustomSound.js delete mode 100644 packages/rocketchat-custom-sounds/client/notifications/updateCustomSound.js delete mode 100644 packages/rocketchat-custom-sounds/package.js delete mode 100644 packages/rocketchat-custom-sounds/server/methods/deleteCustomSound.js delete mode 100644 packages/rocketchat-custom-sounds/server/methods/insertOrUpdateSound.js delete mode 100644 packages/rocketchat-custom-sounds/server/methods/listCustomSounds.js delete mode 100644 packages/rocketchat-custom-sounds/server/methods/uploadCustomSound.js delete mode 100644 packages/rocketchat-custom-sounds/server/models/CustomSounds.js delete mode 100644 packages/rocketchat-custom-sounds/server/publications/customSounds.js delete mode 100644 packages/rocketchat-custom-sounds/server/startup/custom-sounds.js delete mode 100644 packages/rocketchat-custom-sounds/server/startup/permissions.js delete mode 100644 packages/rocketchat-custom-sounds/server/startup/settings.js delete mode 100644 packages/rocketchat-dolphin/common.js delete mode 100644 packages/rocketchat-dolphin/package.js delete mode 100644 packages/rocketchat-dolphin/startup.js delete mode 100644 packages/rocketchat-drupal/common.js delete mode 100644 packages/rocketchat-drupal/package.js delete mode 100644 packages/rocketchat-drupal/startup.js delete mode 100644 packages/rocketchat-e2e/.eslintrc delete mode 100644 packages/rocketchat-e2e/client/helper.js delete mode 100644 packages/rocketchat-e2e/client/rocketchat.e2e.js delete mode 100644 packages/rocketchat-e2e/package.js delete mode 100644 packages/rocketchat-e2e/server/index.js delete mode 100644 packages/rocketchat-e2e/server/methods/addKeyToChain.js delete mode 100644 packages/rocketchat-e2e/server/methods/fetchKeychain.js delete mode 100644 packages/rocketchat-e2e/server/methods/fetchMyKeys.js delete mode 100644 packages/rocketchat-e2e/server/methods/getUsersOfRoomWithoutKey.js delete mode 100644 packages/rocketchat-e2e/server/methods/updateGroupE2EKey.js delete mode 100644 packages/rocketchat-e2e/server/models/Rooms.js delete mode 100644 packages/rocketchat-e2e/server/models/Subscriptions.js delete mode 100644 packages/rocketchat-e2e/server/models/Users.js delete mode 100644 packages/rocketchat-e2e/server/settings.js delete mode 100644 packages/rocketchat-emoji-custom/admin/adminEmoji.html delete mode 100644 packages/rocketchat-emoji-custom/admin/adminEmoji.js delete mode 100644 packages/rocketchat-emoji-custom/admin/route.js delete mode 100644 packages/rocketchat-emoji-custom/admin/startup.js delete mode 100644 packages/rocketchat-emoji-custom/client/lib/emojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/client/models/EmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/client/notifications/deleteEmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/client/notifications/updateEmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/function-isSet.js delete mode 100644 packages/rocketchat-emoji-custom/package.js delete mode 100644 packages/rocketchat-emoji-custom/server/methods/deleteEmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/server/methods/insertOrUpdateEmoji.js delete mode 100644 packages/rocketchat-emoji-custom/server/methods/listEmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/server/methods/uploadEmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/server/models/EmojiCustom.js delete mode 100644 packages/rocketchat-emoji-custom/server/publications/fullEmojiData.js delete mode 100644 packages/rocketchat-emoji-custom/server/startup/emoji-custom.js delete mode 100644 packages/rocketchat-emoji-custom/server/startup/settings.js delete mode 100644 packages/rocketchat-emoji-emojione/README.md delete mode 100644 packages/rocketchat-emoji-emojione/client/sprites.css delete mode 100644 packages/rocketchat-emoji-emojione/emojiPicker.js delete mode 100644 packages/rocketchat-emoji-emojione/emojione.sprites.mustache delete mode 100644 packages/rocketchat-emoji-emojione/generateEmojiIndex.js delete mode 100644 packages/rocketchat-emoji-emojione/package.js delete mode 100644 packages/rocketchat-emoji-emojione/rocketchat.js delete mode 100644 packages/rocketchat-emoji-emojione/server/callbacks.js delete mode 100644 packages/rocketchat-emoji/client/emojiButton.js delete mode 100644 packages/rocketchat-emoji/client/emojiParser.js delete mode 100644 packages/rocketchat-emoji/client/emojiPicker.html delete mode 100644 packages/rocketchat-emoji/client/emojiPicker.js delete mode 100644 packages/rocketchat-emoji/client/function-isSet.js delete mode 100644 packages/rocketchat-emoji/client/keyboardFix.js delete mode 100644 packages/rocketchat-emoji/client/lib/EmojiPicker.js delete mode 100644 packages/rocketchat-emoji/client/lib/emojiRenderer.js delete mode 100644 packages/rocketchat-emoji/client/rocketchat.js delete mode 100644 packages/rocketchat-emoji/package.js delete mode 100644 packages/rocketchat-error-handler/package.js delete mode 100644 packages/rocketchat-error-handler/server/lib/RocketChat.ErrorHandler.js delete mode 100644 packages/rocketchat-error-handler/server/startup/settings.js delete mode 100644 packages/rocketchat-favico/package.js delete mode 100644 packages/rocketchat-file-upload/client/lib/fileUploadHandler.js delete mode 100644 packages/rocketchat-file-upload/globalFileRestrictions.js delete mode 100644 packages/rocketchat-file-upload/lib/FileUpload.js delete mode 100644 packages/rocketchat-file-upload/lib/FileUploadBase.js delete mode 100644 packages/rocketchat-file-upload/package.js delete mode 100644 packages/rocketchat-file-upload/server/config/AmazonS3.js delete mode 100644 packages/rocketchat-file-upload/server/config/Slingshot_DEPRECATED.js delete mode 100644 packages/rocketchat-file-upload/server/config/Webdav.js delete mode 100644 packages/rocketchat-file-upload/server/config/_configUploadStorage.js delete mode 100644 packages/rocketchat-file-upload/server/lib/FileUpload.js delete mode 100644 packages/rocketchat-file-upload/server/lib/proxy.js delete mode 100644 packages/rocketchat-file-upload/server/lib/requests.js delete mode 100644 packages/rocketchat-file-upload/server/methods/getS3FileUrl.js delete mode 100644 packages/rocketchat-file-upload/server/methods/sendFileMessage.js delete mode 100644 packages/rocketchat-file-upload/server/startup/settings.js delete mode 100644 packages/rocketchat-file-upload/ufs/AmazonS3/client.js delete mode 100644 packages/rocketchat-file-upload/ufs/AmazonS3/server.js delete mode 100644 packages/rocketchat-file-upload/ufs/GoogleStorage/client.js delete mode 100644 packages/rocketchat-file-upload/ufs/GoogleStorage/server.js delete mode 100644 packages/rocketchat-file-upload/ufs/Webdav/client.js delete mode 100644 packages/rocketchat-file-upload/ufs/Webdav/server.js delete mode 100644 packages/rocketchat-file/package.js delete mode 100644 packages/rocketchat-github-enterprise/common.js delete mode 100644 packages/rocketchat-github-enterprise/package.js delete mode 100644 packages/rocketchat-github-enterprise/startup.js delete mode 100644 packages/rocketchat-gitlab/common.js delete mode 100644 packages/rocketchat-gitlab/package.js delete mode 100644 packages/rocketchat-gitlab/startup.js delete mode 100644 packages/rocketchat-google-natural-language/server/models/Rooms.js delete mode 100644 packages/rocketchat-google-vision/client/googlevision.js delete mode 100644 packages/rocketchat-google-vision/package.js delete mode 100644 packages/rocketchat-google-vision/server/googlevision.js delete mode 100644 packages/rocketchat-google-vision/server/models/Messages.js delete mode 100644 packages/rocketchat-google-vision/server/settings.js delete mode 100644 packages/rocketchat-grant-facebook/package.js delete mode 100644 packages/rocketchat-grant-facebook/server/index.js delete mode 100644 packages/rocketchat-grant-github/package.js delete mode 100644 packages/rocketchat-grant-github/server/index.js delete mode 100644 packages/rocketchat-grant-google/package.js delete mode 100644 packages/rocketchat-grant-google/server/index.js delete mode 100644 packages/rocketchat-grant/.npm/package/.gitignore delete mode 100644 packages/rocketchat-grant/.npm/package/README delete mode 100644 packages/rocketchat-grant/.npm/package/npm-shrinkwrap.json delete mode 100644 packages/rocketchat-grant/package.js delete mode 100644 packages/rocketchat-grant/server/error.js delete mode 100644 packages/rocketchat-grant/server/index.js delete mode 100644 packages/rocketchat-grant/server/providers.js delete mode 100644 packages/rocketchat-grant/server/settings.js delete mode 100644 packages/rocketchat-graphql/.npm/package/.gitignore delete mode 100644 packages/rocketchat-graphql/.npm/package/README delete mode 100644 packages/rocketchat-graphql/.npm/package/npm-shrinkwrap.json delete mode 100644 packages/rocketchat-graphql/package.js delete mode 100644 packages/rocketchat-graphql/server/api.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/accounts/index.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/channels/Channel-type.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/channels/channels.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/channels/channelsByUser.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/channels/createChannel.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/channels/index.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/messages/deleteMessage.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/messages/messages.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/messages/sendMessage.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/users/User-type.js delete mode 100644 packages/rocketchat-graphql/server/resolvers/users/setStatus.js delete mode 100644 packages/rocketchat-graphql/server/settings.js delete mode 100644 packages/rocketchat-highlight-words/client/client.js delete mode 100644 packages/rocketchat-highlight-words/package.js create mode 100644 packages/rocketchat-i18n/.eslintrc create mode 100644 packages/rocketchat-i18n/i18n/bas-CM.i18n.json create mode 100644 packages/rocketchat-i18n/i18n/et.i18n.json create mode 100644 packages/rocketchat-i18n/i18n/eu.i18n.json create mode 100644 packages/rocketchat-i18n/i18n/hi-IN.i18n.json mode change 100755 => 100644 packages/rocketchat-i18n/i18n/pt-BR.i18n.json mode change 100755 => 100644 packages/rocketchat-i18n/i18n/pt.i18n.json delete mode 100644 packages/rocketchat-iframe-login/iframe_client.js delete mode 100644 packages/rocketchat-iframe-login/iframe_server.js delete mode 100644 packages/rocketchat-iframe-login/package.js delete mode 100644 packages/rocketchat-importer-csv/client/adder.js delete mode 100644 packages/rocketchat-importer-csv/info.js delete mode 100644 packages/rocketchat-importer-csv/package.js delete mode 100644 packages/rocketchat-importer-csv/server/adder.js delete mode 100644 packages/rocketchat-importer-csv/server/importer.js delete mode 100644 packages/rocketchat-importer-hipchat-enterprise/client/adder.js delete mode 100644 packages/rocketchat-importer-hipchat-enterprise/info.js delete mode 100644 packages/rocketchat-importer-hipchat-enterprise/package.js delete mode 100644 packages/rocketchat-importer-hipchat-enterprise/server/adder.js delete mode 100644 packages/rocketchat-importer-hipchat-enterprise/server/importer.js delete mode 100644 packages/rocketchat-importer-hipchat/client/adder.js delete mode 100644 packages/rocketchat-importer-hipchat/info.js delete mode 100644 packages/rocketchat-importer-hipchat/package.js delete mode 100644 packages/rocketchat-importer-hipchat/server/adder.js delete mode 100644 packages/rocketchat-importer-hipchat/server/importer.js delete mode 100644 packages/rocketchat-importer-slack-users/client/adder.js delete mode 100644 packages/rocketchat-importer-slack-users/info.js delete mode 100644 packages/rocketchat-importer-slack-users/package.js delete mode 100644 packages/rocketchat-importer-slack-users/server/adder.js delete mode 100644 packages/rocketchat-importer-slack-users/server/importer.js delete mode 100644 packages/rocketchat-importer-slack/client/adder.js delete mode 100644 packages/rocketchat-importer-slack/info.js delete mode 100644 packages/rocketchat-importer-slack/package.js delete mode 100644 packages/rocketchat-importer-slack/server/adder.js delete mode 100644 packages/rocketchat-importer-slack/server/importer.js delete mode 100644 packages/rocketchat-importer/client/admin/adminImport.html delete mode 100644 packages/rocketchat-importer/client/admin/adminImport.js delete mode 100644 packages/rocketchat-importer/client/admin/adminImportPrepare.html delete mode 100644 packages/rocketchat-importer/client/admin/adminImportPrepare.js delete mode 100644 packages/rocketchat-importer/client/admin/adminImportProgress.html delete mode 100644 packages/rocketchat-importer/client/index.js delete mode 100644 packages/rocketchat-importer/package.js delete mode 100644 packages/rocketchat-importer/server/classes/ImporterBase.js delete mode 100644 packages/rocketchat-importer/server/index.js delete mode 100644 packages/rocketchat-importer/server/methods/restartImport.js delete mode 100644 packages/rocketchat-importer/server/models/Imports.js delete mode 100644 packages/rocketchat-importer/server/models/RawImports.js delete mode 100644 packages/rocketchat-integrations/client/collections.js delete mode 100644 packages/rocketchat-integrations/client/route.js delete mode 100644 packages/rocketchat-integrations/client/startup.js delete mode 100644 packages/rocketchat-integrations/client/views/integrations.js delete mode 100644 packages/rocketchat-integrations/client/views/integrationsNew.js delete mode 100644 packages/rocketchat-integrations/lib/rocketchat.js delete mode 100644 packages/rocketchat-integrations/package.js delete mode 100644 packages/rocketchat-integrations/server/api/api.js delete mode 100644 packages/rocketchat-integrations/server/logger.js delete mode 100644 packages/rocketchat-integrations/server/methods/clearIntegrationHistory.js delete mode 100644 packages/rocketchat-integrations/server/methods/incoming/addIncomingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/methods/incoming/deleteIncomingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/methods/incoming/updateIncomingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/methods/outgoing/addOutgoingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/methods/outgoing/deleteOutgoingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/methods/outgoing/replayOutgoingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/methods/outgoing/updateOutgoingIntegration.js delete mode 100644 packages/rocketchat-integrations/server/models/Integrations.js delete mode 100644 packages/rocketchat-integrations/server/processWebhookMessage.js delete mode 100644 packages/rocketchat-integrations/server/publications/integrationHistory.js delete mode 100644 packages/rocketchat-integrations/server/publications/integrations.js delete mode 100644 packages/rocketchat-integrations/server/triggers.js delete mode 100644 packages/rocketchat-internal-hubot/.npm/package/.gitignore delete mode 100644 packages/rocketchat-internal-hubot/.npm/package/README delete mode 100644 packages/rocketchat-internal-hubot/.npm/package/npm-shrinkwrap.json delete mode 100644 packages/rocketchat-internal-hubot/README.md delete mode 100644 packages/rocketchat-internal-hubot/hubot.js delete mode 100644 packages/rocketchat-internal-hubot/package.js delete mode 100644 packages/rocketchat-internal-hubot/settings.js delete mode 100644 packages/rocketchat-irc/.npm/package/.gitignore delete mode 100644 packages/rocketchat-irc/.npm/package/README delete mode 100644 packages/rocketchat-irc/.npm/package/npm-shrinkwrap.json delete mode 100644 packages/rocketchat-irc/package.js delete mode 100644 packages/rocketchat-irc/server/irc-bridge/index.js delete mode 100644 packages/rocketchat-irc/server/irc-bridge/localHandlers/onCreateRoom.js delete mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/disconnected.js delete mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/joinedChannel.js delete mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/leftChannel.js delete mode 100644 packages/rocketchat-irc/server/irc-bridge/peerHandlers/sentMessage.js delete mode 100644 packages/rocketchat-irc/server/irc.js delete mode 100644 packages/rocketchat-irc/server/methods/resetIrcConnection.js delete mode 100644 packages/rocketchat-irc/server/servers/RFC2813/index.js delete mode 100644 packages/rocketchat-issuelinks/client.js delete mode 100644 packages/rocketchat-issuelinks/package.js delete mode 100644 packages/rocketchat-issuelinks/settings.js delete mode 100644 packages/rocketchat-katex/katex.js delete mode 100644 packages/rocketchat-katex/package-lock.json delete mode 100644 packages/rocketchat-katex/package.js delete mode 100644 packages/rocketchat-katex/package.json delete mode 100644 packages/rocketchat-katex/settings.js delete mode 100644 packages/rocketchat-lazy-load/client/index.js delete mode 100644 packages/rocketchat-lazy-load/client/lazyloadImage.js delete mode 100644 packages/rocketchat-lazy-load/package.js delete mode 100644 packages/rocketchat-ldap/client/loginHelper.js delete mode 100644 packages/rocketchat-ldap/package.js delete mode 100644 packages/rocketchat-ldap/server/loginHandler.js delete mode 100644 packages/rocketchat-ldap/server/settings.js delete mode 100644 packages/rocketchat-ldap/server/syncUsers.js delete mode 100644 packages/rocketchat-lib/README.md delete mode 100644 packages/rocketchat-lib/client/CustomTranslations.js delete mode 100644 packages/rocketchat-lib/client/MessageAction.js delete mode 100644 packages/rocketchat-lib/client/Notifications.js delete mode 100644 packages/rocketchat-lib/client/OAuthProxy.js delete mode 100644 packages/rocketchat-lib/client/UserDeleted.js delete mode 100644 packages/rocketchat-lib/client/defaultTabBars.js delete mode 100644 packages/rocketchat-lib/client/lib/ChannelActions.js delete mode 100644 packages/rocketchat-lib/client/lib/Layout.js delete mode 100644 packages/rocketchat-lib/client/lib/RestApiClient.js delete mode 100644 packages/rocketchat-lib/client/lib/TabBar.js delete mode 100644 packages/rocketchat-lib/client/lib/cachedCollection.js delete mode 100644 packages/rocketchat-lib/client/lib/formatDate.js delete mode 100644 packages/rocketchat-lib/client/lib/index.js delete mode 100644 packages/rocketchat-lib/client/lib/openRoom.js delete mode 100644 packages/rocketchat-lib/client/lib/roomExit.js delete mode 100644 packages/rocketchat-lib/client/lib/roomTypes.js delete mode 100644 packages/rocketchat-lib/client/lib/settings.js delete mode 100644 packages/rocketchat-lib/client/lib/startup/commands.js delete mode 100644 packages/rocketchat-lib/client/methods/sendMessage.js delete mode 100644 packages/rocketchat-lib/client/models/Avatars.js delete mode 100644 packages/rocketchat-lib/client/models/Uploads.js delete mode 100644 packages/rocketchat-lib/client/models/UserDataFiles.js delete mode 100644 packages/rocketchat-lib/client/models/_Base.js delete mode 100644 packages/rocketchat-lib/lib/Message.js delete mode 100644 packages/rocketchat-lib/lib/MessageProperties.js delete mode 100644 packages/rocketchat-lib/lib/MessageTypes.js delete mode 100644 packages/rocketchat-lib/lib/callbacks.js delete mode 100644 packages/rocketchat-lib/lib/core.js delete mode 100644 packages/rocketchat-lib/lib/fileUploadRestrictions.js delete mode 100644 packages/rocketchat-lib/lib/getAvatarColor.js delete mode 100644 packages/rocketchat-lib/lib/getURL.js delete mode 100644 packages/rocketchat-lib/lib/getUserNotificationPreference.js delete mode 100644 packages/rocketchat-lib/lib/getUserPreference.js delete mode 100644 packages/rocketchat-lib/lib/getValidRoomName.js delete mode 100644 packages/rocketchat-lib/lib/messageBox.js delete mode 100644 packages/rocketchat-lib/lib/placeholders.js delete mode 100644 packages/rocketchat-lib/lib/promises.js delete mode 100644 packages/rocketchat-lib/lib/roomTypes/conversation.js delete mode 100644 packages/rocketchat-lib/lib/roomTypes/direct.js delete mode 100644 packages/rocketchat-lib/lib/roomTypes/favorite.js delete mode 100644 packages/rocketchat-lib/lib/roomTypes/private.js delete mode 100644 packages/rocketchat-lib/lib/roomTypes/public.js delete mode 100644 packages/rocketchat-lib/lib/roomTypes/unread.js delete mode 100644 packages/rocketchat-lib/lib/settings.js delete mode 100644 packages/rocketchat-lib/lib/slashCommand.js delete mode 100644 packages/rocketchat-lib/lib/startup/settingsOnLoadSiteUrl.js delete mode 100644 packages/rocketchat-lib/lib/templateVarHandler.js delete mode 100644 packages/rocketchat-lib/package.js delete mode 100644 packages/rocketchat-lib/rocketchat.info delete mode 100644 packages/rocketchat-lib/server/functions/Notifications.js delete mode 100644 packages/rocketchat-lib/server/functions/addUserToDefaultChannels.js delete mode 100644 packages/rocketchat-lib/server/functions/addUserToRoom.js delete mode 100644 packages/rocketchat-lib/server/functions/archiveRoom.js delete mode 100644 packages/rocketchat-lib/server/functions/checkEmailAvailability.js delete mode 100644 packages/rocketchat-lib/server/functions/checkUsernameAvailability.js delete mode 100644 packages/rocketchat-lib/server/functions/cleanRoomHistory.js delete mode 100644 packages/rocketchat-lib/server/functions/createRoom.js delete mode 100644 packages/rocketchat-lib/server/functions/deleteMessage.js delete mode 100644 packages/rocketchat-lib/server/functions/deleteUser.js delete mode 100644 packages/rocketchat-lib/server/functions/getFullUserData.js delete mode 100644 packages/rocketchat-lib/server/functions/getRoomByNameOrIdWithOptionToJoin.js delete mode 100644 packages/rocketchat-lib/server/functions/isDocker.js delete mode 100644 packages/rocketchat-lib/server/functions/loadMessageHistory.js delete mode 100644 packages/rocketchat-lib/server/functions/notifications/audio.js delete mode 100644 packages/rocketchat-lib/server/functions/notifications/desktop.js delete mode 100644 packages/rocketchat-lib/server/functions/notifications/email.js delete mode 100644 packages/rocketchat-lib/server/functions/notifications/index.js delete mode 100644 packages/rocketchat-lib/server/functions/notifications/mobile.js delete mode 100644 packages/rocketchat-lib/server/functions/removeUserFromRoom.js delete mode 100644 packages/rocketchat-lib/server/functions/saveCustomFields.js delete mode 100644 packages/rocketchat-lib/server/functions/saveCustomFieldsWithoutValidation.js delete mode 100644 packages/rocketchat-lib/server/functions/saveUser.js delete mode 100644 packages/rocketchat-lib/server/functions/sendMessage.js delete mode 100644 packages/rocketchat-lib/server/functions/setEmail.js delete mode 100644 packages/rocketchat-lib/server/functions/setRealName.js delete mode 100644 packages/rocketchat-lib/server/functions/setUsername.js delete mode 100644 packages/rocketchat-lib/server/functions/settings.js delete mode 100644 packages/rocketchat-lib/server/functions/unarchiveRoom.js delete mode 100644 packages/rocketchat-lib/server/functions/updateMessage.js delete mode 100644 packages/rocketchat-lib/server/lib/PushNotification.js delete mode 100644 packages/rocketchat-lib/server/lib/RateLimiter.js delete mode 100644 packages/rocketchat-lib/server/lib/bugsnag.js delete mode 100644 packages/rocketchat-lib/server/lib/configLogger.js delete mode 100644 packages/rocketchat-lib/server/lib/debug.js delete mode 100644 packages/rocketchat-lib/server/lib/index.js delete mode 100644 packages/rocketchat-lib/server/lib/metrics.js delete mode 100644 packages/rocketchat-lib/server/lib/notifyUsersOnMessage.js delete mode 100644 packages/rocketchat-lib/server/lib/passwordPolicy.js delete mode 100644 packages/rocketchat-lib/server/lib/processDirectEmail.js delete mode 100644 packages/rocketchat-lib/server/lib/roomTypes.js delete mode 100644 packages/rocketchat-lib/server/lib/sendNotificationsOnMessage.js delete mode 100644 packages/rocketchat-lib/server/lib/validateEmailDomain.js delete mode 100644 packages/rocketchat-lib/server/methods/addOAuthService.js delete mode 100644 packages/rocketchat-lib/server/methods/addUserToRoom.js delete mode 100644 packages/rocketchat-lib/server/methods/addUsersToRoom.js delete mode 100644 packages/rocketchat-lib/server/methods/archiveRoom.js delete mode 100644 packages/rocketchat-lib/server/methods/blockUser.js delete mode 100644 packages/rocketchat-lib/server/methods/checkRegistrationSecretURL.js delete mode 100644 packages/rocketchat-lib/server/methods/checkUsernameAvailability.js delete mode 100644 packages/rocketchat-lib/server/methods/cleanRoomHistory.js delete mode 100644 packages/rocketchat-lib/server/methods/createChannel.js delete mode 100644 packages/rocketchat-lib/server/methods/createPrivateGroup.js delete mode 100644 packages/rocketchat-lib/server/methods/createToken.js delete mode 100644 packages/rocketchat-lib/server/methods/deleteMessage.js delete mode 100644 packages/rocketchat-lib/server/methods/deleteUserOwnAccount.js delete mode 100644 packages/rocketchat-lib/server/methods/executeSlashCommandPreview.js delete mode 100644 packages/rocketchat-lib/server/methods/filterATAllTag.js delete mode 100644 packages/rocketchat-lib/server/methods/filterATHereTag.js delete mode 100644 packages/rocketchat-lib/server/methods/filterBadWords.js delete mode 100644 packages/rocketchat-lib/server/methods/getChannelHistory.js delete mode 100644 packages/rocketchat-lib/server/methods/getFullUserData.js delete mode 100644 packages/rocketchat-lib/server/methods/getRoomJoinCode.js delete mode 100644 packages/rocketchat-lib/server/methods/getRoomRoles.js delete mode 100644 packages/rocketchat-lib/server/methods/getServerInfo.js delete mode 100644 packages/rocketchat-lib/server/methods/getSingleMessage.js delete mode 100644 packages/rocketchat-lib/server/methods/getSlashCommandPreviews.js delete mode 100644 packages/rocketchat-lib/server/methods/getUserRoles.js delete mode 100644 packages/rocketchat-lib/server/methods/insertOrUpdateUser.js delete mode 100644 packages/rocketchat-lib/server/methods/joinDefaultChannels.js delete mode 100644 packages/rocketchat-lib/server/methods/joinRoom.js delete mode 100644 packages/rocketchat-lib/server/methods/leaveRoom.js delete mode 100644 packages/rocketchat-lib/server/methods/refreshOAuthService.js delete mode 100644 packages/rocketchat-lib/server/methods/removeOAuthService.js delete mode 100644 packages/rocketchat-lib/server/methods/robotMethods.js delete mode 100644 packages/rocketchat-lib/server/methods/saveSetting.js delete mode 100644 packages/rocketchat-lib/server/methods/sendInvitationEmail.js delete mode 100644 packages/rocketchat-lib/server/methods/sendMessage.js delete mode 100644 packages/rocketchat-lib/server/methods/sendSMTPTestEmail.js delete mode 100644 packages/rocketchat-lib/server/methods/setAdminStatus.js delete mode 100644 packages/rocketchat-lib/server/methods/setEmail.js delete mode 100644 packages/rocketchat-lib/server/methods/setRealName.js delete mode 100644 packages/rocketchat-lib/server/methods/setUsername.js delete mode 100644 packages/rocketchat-lib/server/methods/unarchiveRoom.js delete mode 100644 packages/rocketchat-lib/server/methods/unblockUser.js delete mode 100644 packages/rocketchat-lib/server/methods/updateMessage.js delete mode 100644 packages/rocketchat-lib/server/models/Avatars.js delete mode 100644 packages/rocketchat-lib/server/models/Messages.js delete mode 100644 packages/rocketchat-lib/server/models/Rooms.js delete mode 100644 packages/rocketchat-lib/server/models/Settings.js delete mode 100644 packages/rocketchat-lib/server/models/Subscriptions.js delete mode 100644 packages/rocketchat-lib/server/models/Uploads.js delete mode 100644 packages/rocketchat-lib/server/models/UserDataFiles.js delete mode 100644 packages/rocketchat-lib/server/models/Users.js delete mode 100644 packages/rocketchat-lib/server/models/_Base.js delete mode 100644 packages/rocketchat-lib/server/models/_BaseDb.js delete mode 100644 packages/rocketchat-lib/server/oauth/facebook.js delete mode 100644 packages/rocketchat-lib/server/oauth/oauth.js delete mode 100644 packages/rocketchat-lib/server/oauth/proxy.js delete mode 100644 packages/rocketchat-lib/server/publications/settings.js delete mode 100644 packages/rocketchat-lib/server/startup/oAuthServicesUpdate.js delete mode 100644 packages/rocketchat-lib/server/startup/settings.js delete mode 100644 packages/rocketchat-lib/server/startup/settingsOnLoadCdnPrefix.js delete mode 100644 packages/rocketchat-lib/server/startup/settingsOnLoadDirectReply.js delete mode 100644 packages/rocketchat-lib/server/startup/settingsOnLoadSMTP.js delete mode 100644 packages/rocketchat-lib/startup/defaultRoomTypes.js delete mode 100644 packages/rocketchat-lib/tests/server.tests.js create mode 100644 packages/rocketchat-livechat/.app/.eslintrc create mode 100644 packages/rocketchat-livechat/.app/i18n/bas-CM.i18n.json create mode 100644 packages/rocketchat-livechat/.app/i18n/et.i18n.json create mode 100644 packages/rocketchat-livechat/.app/i18n/eu.i18n.json create mode 100644 packages/rocketchat-livechat/.app/i18n/hi-IN.i18n.json delete mode 100644 packages/rocketchat-livechat/client/collections/AgentUsers.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatCustomField.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatDepartment.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatDepartmentAgents.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatInquiry.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatIntegration.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatMonitoring.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatPageVisited.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatQueueUser.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatTrigger.js delete mode 100644 packages/rocketchat-livechat/client/collections/LivechatVisitor.js delete mode 100644 packages/rocketchat-livechat/client/collections/livechatOfficeHour.js delete mode 100644 packages/rocketchat-livechat/client/methods/changeLivechatStatus.js delete mode 100644 packages/rocketchat-livechat/client/roomType.js delete mode 100644 packages/rocketchat-livechat/client/route.js delete mode 100644 packages/rocketchat-livechat/client/startup/notifyUnreadRooms.js delete mode 100644 packages/rocketchat-livechat/client/ui.js delete mode 100644 packages/rocketchat-livechat/client/views/app/integrations/livechatIntegrationWebhook.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatAppearance.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatAppearance.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatCurrentChats.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatCurrentChats.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatCustomFields.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatCustomFields.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatDepartmentForm.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatDepartments.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatDepartments.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatInstallation.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatInstallation.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatIntegrations.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatOfficeHours.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatOfficeHours.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatQueue.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatTriggers.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatTriggers.js delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatUsers.html delete mode 100644 packages/rocketchat-livechat/client/views/app/livechatUsers.js delete mode 100644 packages/rocketchat-livechat/client/views/app/tabbar/externalSearch.js delete mode 100644 packages/rocketchat-livechat/client/views/app/tabbar/visitorForward.js delete mode 100644 packages/rocketchat-livechat/client/views/app/tabbar/visitorHistory.js delete mode 100644 packages/rocketchat-livechat/client/views/app/tabbar/visitorInfo.js delete mode 100644 packages/rocketchat-livechat/client/views/sideNav/livechat.js delete mode 100644 packages/rocketchat-livechat/client/views/sideNav/livechatFlex.js delete mode 100644 packages/rocketchat-livechat/config.js delete mode 100644 packages/rocketchat-livechat/imports/LivechatRoomType.js delete mode 100644 packages/rocketchat-livechat/imports/server/rest/departments.js delete mode 100644 packages/rocketchat-livechat/imports/server/rest/facebook.js delete mode 100644 packages/rocketchat-livechat/imports/server/rest/upload.js delete mode 100644 packages/rocketchat-livechat/imports/server/rest/users.js delete mode 100644 packages/rocketchat-livechat/livechat.js delete mode 100644 packages/rocketchat-livechat/messageTypes.js delete mode 100644 packages/rocketchat-livechat/permissions.js delete mode 100644 packages/rocketchat-livechat/server/api.js delete mode 100644 packages/rocketchat-livechat/server/api/lib/livechat.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/agent.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/config.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/customField.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/message.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/offlineMessage.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/pageVisited.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/room.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/transcript.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/videoCall.js delete mode 100644 packages/rocketchat-livechat/server/api/v1/visitor.js delete mode 100644 packages/rocketchat-livechat/server/hooks/RDStation.js delete mode 100644 packages/rocketchat-livechat/server/hooks/externalMessage.js delete mode 100644 packages/rocketchat-livechat/server/hooks/leadCapture.js delete mode 100644 packages/rocketchat-livechat/server/hooks/markRoomResponded.js delete mode 100644 packages/rocketchat-livechat/server/hooks/offlineMessage.js delete mode 100644 packages/rocketchat-livechat/server/hooks/saveAnalyticsData.js delete mode 100644 packages/rocketchat-livechat/server/hooks/sendToCRM.js delete mode 100644 packages/rocketchat-livechat/server/hooks/sendToFacebook.js delete mode 100644 packages/rocketchat-livechat/server/lib/Livechat.js delete mode 100644 packages/rocketchat-livechat/server/lib/OfficeClock.js delete mode 100644 packages/rocketchat-livechat/server/lib/OmniChannel.js delete mode 100644 packages/rocketchat-livechat/server/lib/QueueMethods.js delete mode 100644 packages/rocketchat-livechat/server/methods/addAgent.js delete mode 100644 packages/rocketchat-livechat/server/methods/addManager.js delete mode 100644 packages/rocketchat-livechat/server/methods/changeLivechatStatus.js delete mode 100644 packages/rocketchat-livechat/server/methods/closeByVisitor.js delete mode 100644 packages/rocketchat-livechat/server/methods/closeRoom.js delete mode 100644 packages/rocketchat-livechat/server/methods/facebook.js delete mode 100644 packages/rocketchat-livechat/server/methods/getAgentData.js delete mode 100644 packages/rocketchat-livechat/server/methods/getAgentOverviewData.js delete mode 100644 packages/rocketchat-livechat/server/methods/getAnalyticsChartData.js delete mode 100644 packages/rocketchat-livechat/server/methods/getAnalyticsOverviewData.js delete mode 100644 packages/rocketchat-livechat/server/methods/getCustomFields.js delete mode 100644 packages/rocketchat-livechat/server/methods/getInitialData.js delete mode 100644 packages/rocketchat-livechat/server/methods/getNextAgent.js delete mode 100644 packages/rocketchat-livechat/server/methods/loadHistory.js delete mode 100644 packages/rocketchat-livechat/server/methods/loginByToken.js delete mode 100644 packages/rocketchat-livechat/server/methods/pageVisited.js delete mode 100644 packages/rocketchat-livechat/server/methods/registerGuest.js delete mode 100644 packages/rocketchat-livechat/server/methods/removeAgent.js delete mode 100644 packages/rocketchat-livechat/server/methods/removeCustomField.js delete mode 100644 packages/rocketchat-livechat/server/methods/removeDepartment.js delete mode 100644 packages/rocketchat-livechat/server/methods/removeManager.js delete mode 100644 packages/rocketchat-livechat/server/methods/removeRoom.js delete mode 100644 packages/rocketchat-livechat/server/methods/removeTrigger.js delete mode 100644 packages/rocketchat-livechat/server/methods/returnAsInquiry.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveAppearance.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveCustomField.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveDepartment.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveInfo.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveIntegration.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveOfficeHours.js delete mode 100644 packages/rocketchat-livechat/server/methods/saveTrigger.js delete mode 100644 packages/rocketchat-livechat/server/methods/searchAgent.js delete mode 100644 packages/rocketchat-livechat/server/methods/sendMessageLivechat.js delete mode 100644 packages/rocketchat-livechat/server/methods/sendOfflineMessage.js delete mode 100644 packages/rocketchat-livechat/server/methods/sendTranscript.js delete mode 100644 packages/rocketchat-livechat/server/methods/setCustomField.js delete mode 100644 packages/rocketchat-livechat/server/methods/setDepartmentForVisitor.js delete mode 100644 packages/rocketchat-livechat/server/methods/startFileUploadRoom.js delete mode 100644 packages/rocketchat-livechat/server/methods/startVideoCall.js delete mode 100644 packages/rocketchat-livechat/server/methods/takeInquiry.js delete mode 100644 packages/rocketchat-livechat/server/methods/transfer.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatCustomField.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatDepartment.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatDepartmentAgents.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatExternalMessage.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatInquiry.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatPageVisited.js delete mode 100644 packages/rocketchat-livechat/server/models/LivechatTrigger.js delete mode 100644 packages/rocketchat-livechat/server/models/Messages.js delete mode 100644 packages/rocketchat-livechat/server/models/Rooms.js delete mode 100644 packages/rocketchat-livechat/server/models/Users.js delete mode 100644 packages/rocketchat-livechat/server/models/indexes.js delete mode 100644 packages/rocketchat-livechat/server/publications/customFields.js delete mode 100644 packages/rocketchat-livechat/server/publications/departmentAgents.js delete mode 100644 packages/rocketchat-livechat/server/publications/externalMessages.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatAgents.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatAppearance.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatDepartments.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatInquiries.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatIntegration.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatManagers.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatMonitoring.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatOfficeHours.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatQueue.js delete mode 100644 packages/rocketchat-livechat/server/publications/livechatTriggers.js delete mode 100644 packages/rocketchat-livechat/server/publications/visitorHistory.js delete mode 100644 packages/rocketchat-livechat/server/publications/visitorInfo.js delete mode 100644 packages/rocketchat-livechat/server/publications/visitorPageVisited.js delete mode 100644 packages/rocketchat-livechat/server/roomType.js delete mode 100644 packages/rocketchat-livechat/server/sendMessageBySMS.js delete mode 100644 packages/rocketchat-livechat/server/startup.js delete mode 100644 packages/rocketchat-livechat/server/unclosedLivechats.js delete mode 100644 packages/rocketchat-livechat/server/visitorStatus.js delete mode 100644 packages/rocketchat-livestream/client/oauth.js delete mode 100644 packages/rocketchat-livestream/client/tabBar.js delete mode 100644 packages/rocketchat-livestream/package.js delete mode 100644 packages/rocketchat-livestream/server/functions/saveStreamingOptions.js delete mode 100644 packages/rocketchat-livestream/server/index.js delete mode 100644 packages/rocketchat-livestream/server/methods.js delete mode 100644 packages/rocketchat-livestream/server/models/Rooms.js delete mode 100644 packages/rocketchat-livestream/server/routes.js delete mode 100644 packages/rocketchat-livestream/server/settings.js delete mode 100644 packages/rocketchat-logger/client/ansispan.js delete mode 100644 packages/rocketchat-logger/client/logger.js delete mode 100644 packages/rocketchat-logger/client/viewLogs.js delete mode 100644 packages/rocketchat-logger/client/views/viewLogs.html delete mode 100644 packages/rocketchat-logger/client/views/viewLogs.js delete mode 100644 packages/rocketchat-logger/package.js delete mode 100644 packages/rocketchat-logger/server/server.js delete mode 100644 packages/rocketchat-mailer/client/router.js delete mode 100644 packages/rocketchat-mailer/client/startup.js delete mode 100644 packages/rocketchat-mailer/client/views/mailer.js delete mode 100644 packages/rocketchat-mailer/client/views/mailerUnsubscribe.js delete mode 100644 packages/rocketchat-mailer/lib/Mailer.js delete mode 100644 packages/rocketchat-mailer/package.js delete mode 100644 packages/rocketchat-mailer/server/functions/sendMail.js delete mode 100644 packages/rocketchat-mailer/server/functions/unsubscribe.js delete mode 100644 packages/rocketchat-mailer/server/methods/sendMail.js delete mode 100644 packages/rocketchat-mailer/server/methods/unsubscribe.js delete mode 100644 packages/rocketchat-mailer/server/models/Users.js delete mode 100644 packages/rocketchat-mailer/server/startup.js delete mode 100644 packages/rocketchat-mapview/package.js delete mode 100644 packages/rocketchat-mapview/server/settings.js delete mode 100644 packages/rocketchat-markdown/markdown.js delete mode 100644 packages/rocketchat-markdown/package.js delete mode 100644 packages/rocketchat-markdown/parser/marked/marked.js delete mode 100644 packages/rocketchat-markdown/parser/original/markdown.js delete mode 100644 packages/rocketchat-markdown/settings.js delete mode 100644 packages/rocketchat-markdown/tests/client.mocks.js delete mode 100644 packages/rocketchat-markdown/tests/client.tests.js delete mode 100644 packages/rocketchat-mentions-flextab/client/actionButton.js delete mode 100644 packages/rocketchat-mentions-flextab/client/lib/MentionedMessage.js delete mode 100644 packages/rocketchat-mentions-flextab/client/tabBar.js delete mode 100644 packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.html delete mode 100644 packages/rocketchat-mentions-flextab/client/views/mentionsFlexTab.js delete mode 100644 packages/rocketchat-mentions-flextab/client/views/stylesheets/mentionsFlexTab.less delete mode 100644 packages/rocketchat-mentions-flextab/package.js delete mode 100644 packages/rocketchat-mentions-flextab/server/publications/mentionedMessages.js delete mode 100644 packages/rocketchat-mentions/Mentions.js delete mode 100644 packages/rocketchat-mentions/client/client.js delete mode 100644 packages/rocketchat-mentions/package.js delete mode 100644 packages/rocketchat-mentions/server/Mentions.js delete mode 100644 packages/rocketchat-mentions/server/methods/getUserMentionsByChannel.js delete mode 100644 packages/rocketchat-mentions/server/server.js delete mode 100644 packages/rocketchat-mentions/tests/client.tests.js delete mode 100644 packages/rocketchat-mentions/tests/server.tests.js delete mode 100644 packages/rocketchat-message-action/client/messageAction.js delete mode 100644 packages/rocketchat-message-action/package.js delete mode 100644 packages/rocketchat-message-attachments/client/messageAttachment.html delete mode 100644 packages/rocketchat-message-attachments/client/messageAttachment.js delete mode 100644 packages/rocketchat-message-attachments/package.js delete mode 100644 packages/rocketchat-message-mark-as-unread/client/actionButton.js delete mode 100644 packages/rocketchat-message-mark-as-unread/package.js delete mode 100644 packages/rocketchat-message-mark-as-unread/server/logger.js delete mode 100644 packages/rocketchat-message-mark-as-unread/server/unreadMessages.js delete mode 100644 packages/rocketchat-message-pin/client/actionButton.js delete mode 100644 packages/rocketchat-message-pin/client/lib/PinnedMessage.js delete mode 100644 packages/rocketchat-message-pin/client/messageType.js delete mode 100644 packages/rocketchat-message-pin/client/pinMessage.js delete mode 100644 packages/rocketchat-message-pin/client/tabBar.js delete mode 100644 packages/rocketchat-message-pin/client/views/pinnedMessages.html delete mode 100644 packages/rocketchat-message-pin/client/views/pinnedMessages.js delete mode 100644 packages/rocketchat-message-pin/package.js delete mode 100644 packages/rocketchat-message-pin/server/pinMessage.js delete mode 100644 packages/rocketchat-message-pin/server/publications/pinnedMessages.js delete mode 100644 packages/rocketchat-message-pin/server/settings.js delete mode 100644 packages/rocketchat-message-pin/server/startup/indexes.js delete mode 100644 packages/rocketchat-message-snippet/client/actionButton.js delete mode 100644 packages/rocketchat-message-snippet/client/lib/collections.js delete mode 100644 packages/rocketchat-message-snippet/client/messageType.js delete mode 100644 packages/rocketchat-message-snippet/client/page/snippetPage.js delete mode 100644 packages/rocketchat-message-snippet/client/router.js delete mode 100644 packages/rocketchat-message-snippet/client/snippetMessage.js delete mode 100644 packages/rocketchat-message-snippet/client/tabBar/tabBar.js delete mode 100644 packages/rocketchat-message-snippet/client/tabBar/views/snippetedMessages.html delete mode 100644 packages/rocketchat-message-snippet/client/tabBar/views/snippetedMessages.js delete mode 100644 packages/rocketchat-message-snippet/package.js delete mode 100644 packages/rocketchat-message-snippet/server/methods/snippetMessage.js delete mode 100644 packages/rocketchat-message-snippet/server/publications/snippetedMessage.js delete mode 100644 packages/rocketchat-message-snippet/server/publications/snippetedMessagesByRoom.js delete mode 100644 packages/rocketchat-message-snippet/server/requests.js delete mode 100644 packages/rocketchat-message-snippet/server/startup/settings.js delete mode 100644 packages/rocketchat-message-star/client/actionButton.js delete mode 100644 packages/rocketchat-message-star/client/lib/StarredMessage.js delete mode 100644 packages/rocketchat-message-star/client/starMessage.js delete mode 100644 packages/rocketchat-message-star/client/tabBar.js delete mode 100644 packages/rocketchat-message-star/client/views/starredMessages.html delete mode 100644 packages/rocketchat-message-star/client/views/starredMessages.js delete mode 100644 packages/rocketchat-message-star/package.js delete mode 100644 packages/rocketchat-message-star/server/publications/starredMessages.js delete mode 100644 packages/rocketchat-message-star/server/settings.js delete mode 100644 packages/rocketchat-message-star/server/starMessage.js delete mode 100644 packages/rocketchat-message-star/server/startup/indexes.js delete mode 100644 packages/rocketchat-migrations/package.js create mode 100644 packages/rocketchat-mongo-config/package.js create mode 100644 packages/rocketchat-mongo-config/server/index.js delete mode 100755 packages/rocketchat-nrr/package.js delete mode 100644 packages/rocketchat-oauth2-server-config/.gitignore delete mode 100644 packages/rocketchat-oauth2-server-config/admin/client/collection.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/client/route.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/client/startup.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.html delete mode 100644 packages/rocketchat-oauth2-server-config/admin/client/views/oauthApps.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/server/methods/addOAuthApp.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/server/methods/deleteOAuthApp.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/server/methods/updateOAuthApp.js delete mode 100644 packages/rocketchat-oauth2-server-config/admin/server/publications/oauthApps.js delete mode 100644 packages/rocketchat-oauth2-server-config/oauth/server/default-services.js delete mode 100644 packages/rocketchat-oauth2-server-config/package.js delete mode 100644 packages/rocketchat-oauth2-server-config/server/models/OAuthApps.js delete mode 100644 packages/rocketchat-oembed/client/oembedAudioWidget.js delete mode 100644 packages/rocketchat-oembed/client/oembedFrameWidget.js delete mode 100644 packages/rocketchat-oembed/client/oembedImageWidget.js delete mode 100644 packages/rocketchat-oembed/client/oembedSandstormGrain.html delete mode 100644 packages/rocketchat-oembed/client/oembedSandstormGrain.js delete mode 100644 packages/rocketchat-oembed/client/oembedVideoWidget.js delete mode 100644 packages/rocketchat-oembed/client/oembedYoutubeWidget.js delete mode 100644 packages/rocketchat-oembed/package.js delete mode 100644 packages/rocketchat-oembed/server/jumpToMessage.js delete mode 100644 packages/rocketchat-oembed/server/providers.js delete mode 100644 packages/rocketchat-oembed/server/server.js delete mode 100644 packages/rocketchat-otr/client/rocketchat.otr.js delete mode 100644 packages/rocketchat-otr/client/tabBar.js delete mode 100644 packages/rocketchat-otr/client/views/otrFlexTab.js delete mode 100644 packages/rocketchat-otr/package.js delete mode 100644 packages/rocketchat-otr/server/methods/deleteOldOTRMessages.js delete mode 100644 packages/rocketchat-otr/server/methods/updateOTRAck.js delete mode 100644 packages/rocketchat-otr/server/models/Messages.js delete mode 100644 packages/rocketchat-otr/server/settings.js delete mode 100644 packages/rocketchat-postcss/.npm/plugin/minifier-postcss/.gitignore delete mode 100644 packages/rocketchat-postcss/.npm/plugin/minifier-postcss/README delete mode 100644 packages/rocketchat-postcss/.npm/plugin/minifier-postcss/npm-shrinkwrap.json delete mode 100644 packages/rocketchat-postcss/package.js delete mode 100644 packages/rocketchat-postcss/plugin/minify-css.js delete mode 100644 packages/rocketchat-push-notifications/client/tabBar.js delete mode 100644 packages/rocketchat-push-notifications/package.js delete mode 100644 packages/rocketchat-push-notifications/server/methods/saveNotificationSettings.js delete mode 100644 packages/rocketchat-push-notifications/server/models/Subscriptions.js delete mode 100644 packages/rocketchat-reactions/client/init.js delete mode 100644 packages/rocketchat-reactions/client/methods/setReaction.js delete mode 100644 packages/rocketchat-reactions/package.js delete mode 100644 packages/rocketchat-reactions/server/models/Messages.js delete mode 100644 packages/rocketchat-reactions/setReaction.js delete mode 100644 packages/rocketchat-retention-policy/package.js delete mode 100644 packages/rocketchat-retention-policy/server/cronPruneMessages.js delete mode 100644 packages/rocketchat-retention-policy/server/startup/settings.js delete mode 100644 packages/rocketchat-sandstorm/client/powerboxListener.js delete mode 100644 packages/rocketchat-sandstorm/client/setPath.js delete mode 100644 packages/rocketchat-sandstorm/package.js delete mode 100644 packages/rocketchat-sandstorm/server/events.js delete mode 100644 packages/rocketchat-sandstorm/server/lib.js delete mode 100644 packages/rocketchat-sandstorm/server/powerbox.js delete mode 100644 packages/rocketchat-search/client/provider/result.html delete mode 100644 packages/rocketchat-search/client/provider/result.js delete mode 100644 packages/rocketchat-search/package.js delete mode 100644 packages/rocketchat-search/server/events/events.js delete mode 100644 packages/rocketchat-search/server/index.js delete mode 100644 packages/rocketchat-search/server/logger/logger.js delete mode 100644 packages/rocketchat-search/server/model/provider.js delete mode 100644 packages/rocketchat-setup-wizard/client/final.js delete mode 100644 packages/rocketchat-setup-wizard/client/setupWizard.js delete mode 100644 packages/rocketchat-setup-wizard/package.js delete mode 100644 packages/rocketchat-setup-wizard/server/getSetupWizardParameters.js delete mode 100644 packages/rocketchat-slackbridge/client/slackbridge_import.client.js delete mode 100644 packages/rocketchat-slackbridge/package.js delete mode 100644 packages/rocketchat-slackbridge/server/RocketAdapter.js delete mode 100644 packages/rocketchat-slackbridge/server/logger.js delete mode 100644 packages/rocketchat-slackbridge/server/settings.js delete mode 100644 packages/rocketchat-slackbridge/server/slackbridge.js delete mode 100644 packages/rocketchat-slackbridge/server/slackbridge_import.server.js delete mode 100644 packages/rocketchat-slashcommand-asciiarts/lenny.js delete mode 100644 packages/rocketchat-slashcommand-asciiarts/package.js delete mode 100644 packages/rocketchat-slashcommands-archiveroom/client/client.js delete mode 100644 packages/rocketchat-slashcommands-archiveroom/package.js delete mode 100644 packages/rocketchat-slashcommands-archiveroom/server/messages.js delete mode 100644 packages/rocketchat-slashcommands-archiveroom/server/server.js delete mode 100644 packages/rocketchat-slashcommands-create/client/client.js delete mode 100644 packages/rocketchat-slashcommands-create/package.js delete mode 100644 packages/rocketchat-slashcommands-create/server/server.js delete mode 100644 packages/rocketchat-slashcommands-help/package.js delete mode 100644 packages/rocketchat-slashcommands-help/server.js delete mode 100644 packages/rocketchat-slashcommands-hide/client/hide.js delete mode 100644 packages/rocketchat-slashcommands-hide/package.js delete mode 100644 packages/rocketchat-slashcommands-hide/server/hide.js delete mode 100644 packages/rocketchat-slashcommands-invite/client/client.js delete mode 100644 packages/rocketchat-slashcommands-invite/package.js delete mode 100644 packages/rocketchat-slashcommands-invite/server/server.js delete mode 100644 packages/rocketchat-slashcommands-inviteall/client/client.js delete mode 100644 packages/rocketchat-slashcommands-inviteall/package.js delete mode 100644 packages/rocketchat-slashcommands-inviteall/server/server.js delete mode 100644 packages/rocketchat-slashcommands-join/client/client.js delete mode 100644 packages/rocketchat-slashcommands-join/package.js delete mode 100644 packages/rocketchat-slashcommands-join/server/server.js delete mode 100644 packages/rocketchat-slashcommands-kick/client/client.js delete mode 100644 packages/rocketchat-slashcommands-kick/package.js delete mode 100644 packages/rocketchat-slashcommands-kick/server/server.js delete mode 100644 packages/rocketchat-slashcommands-leave/leave.js delete mode 100644 packages/rocketchat-slashcommands-leave/package.js delete mode 100644 packages/rocketchat-slashcommands-me/me.js delete mode 100644 packages/rocketchat-slashcommands-me/package.js delete mode 100644 packages/rocketchat-slashcommands-msg/package.js delete mode 100644 packages/rocketchat-slashcommands-msg/server.js delete mode 100644 packages/rocketchat-slashcommands-mute/package.js delete mode 100644 packages/rocketchat-slashcommands-mute/server/mute.js delete mode 100644 packages/rocketchat-slashcommands-mute/server/unmute.js delete mode 100644 packages/rocketchat-slashcommands-open/client/client.js delete mode 100644 packages/rocketchat-slashcommands-open/package.js delete mode 100644 packages/rocketchat-slashcommands-topic/package.js delete mode 100644 packages/rocketchat-slashcommands-topic/topic.js delete mode 100644 packages/rocketchat-slashcommands-unarchiveroom/client/client.js delete mode 100644 packages/rocketchat-slashcommands-unarchiveroom/package.js delete mode 100644 packages/rocketchat-slashcommands-unarchiveroom/server/messages.js delete mode 100644 packages/rocketchat-slashcommands-unarchiveroom/server/server.js delete mode 100644 packages/rocketchat-slider/package.js delete mode 100644 packages/rocketchat-smarsh-connector/lib/rocketchat.js delete mode 100644 packages/rocketchat-smarsh-connector/package.js delete mode 100644 packages/rocketchat-smarsh-connector/server/functions/sendEmail.js delete mode 100644 packages/rocketchat-smarsh-connector/server/models/SmarshHistory.js delete mode 100644 packages/rocketchat-smarsh-connector/server/settings.js delete mode 100644 packages/rocketchat-smarsh-connector/server/startup.js delete mode 100644 packages/rocketchat-sms/SMS.js delete mode 100644 packages/rocketchat-sms/package.js delete mode 100644 packages/rocketchat-sms/settings.js delete mode 100644 packages/rocketchat-spotify/package.js delete mode 100644 packages/rocketchat-statistics/lib/rocketchat.js delete mode 100644 packages/rocketchat-statistics/package.js delete mode 100644 packages/rocketchat-statistics/server/functions/get.js delete mode 100644 packages/rocketchat-statistics/server/functions/save.js delete mode 100644 packages/rocketchat-statistics/server/methods/getStatistics.js delete mode 100644 packages/rocketchat-theme/client/imports/components/badge.css delete mode 100644 packages/rocketchat-theme/client/imports/components/contextual-bar.css delete mode 100644 packages/rocketchat-theme/client/imports/components/header.css delete mode 100644 packages/rocketchat-theme/client/imports/components/message-box.css delete mode 100644 packages/rocketchat-theme/client/imports/components/messages.css delete mode 100644 packages/rocketchat-theme/client/imports/components/modal/create-channel.css delete mode 100644 packages/rocketchat-theme/client/imports/components/modal/directory.css delete mode 100644 packages/rocketchat-theme/client/imports/components/tabs.css delete mode 100644 packages/rocketchat-theme/client/imports/forms/button.css delete mode 100644 packages/rocketchat-theme/client/imports/general/apps.css delete mode 100644 packages/rocketchat-theme/client/imports/general/rtl.css delete mode 100644 packages/rocketchat-theme/client/imports/general/variables.css delete mode 100644 packages/rocketchat-theme/package.js delete mode 100644 packages/rocketchat-theme/server/server.js delete mode 100644 packages/rocketchat-token-login/client/login_token_client.js delete mode 100644 packages/rocketchat-token-login/package.js delete mode 100644 packages/rocketchat-tokenpass/client/roomType.js delete mode 100644 packages/rocketchat-tokenpass/client/startup.js delete mode 100644 packages/rocketchat-tokenpass/client/tokenChannelsList.js delete mode 100644 packages/rocketchat-tokenpass/common.js delete mode 100644 packages/rocketchat-tokenpass/package.js delete mode 100644 packages/rocketchat-tokenpass/server/cronRemoveUsers.js delete mode 100644 packages/rocketchat-tokenpass/server/functions/getProtectedTokenpassBalances.js delete mode 100644 packages/rocketchat-tokenpass/server/functions/getPublicTokenpassBalances.js delete mode 100644 packages/rocketchat-tokenpass/server/functions/saveRoomTokens.js delete mode 100644 packages/rocketchat-tokenpass/server/functions/saveRoomTokensMinimumBalance.js delete mode 100644 packages/rocketchat-tokenpass/server/functions/updateUserTokenpassBalances.js delete mode 100644 packages/rocketchat-tokenpass/server/methods/findTokenChannels.js delete mode 100644 packages/rocketchat-tokenpass/server/methods/getChannelTokenpass.js delete mode 100644 packages/rocketchat-tokenpass/server/models/Rooms.js delete mode 100644 packages/rocketchat-tokenpass/server/models/Subscriptions.js delete mode 100644 packages/rocketchat-tokenpass/server/models/Users.js delete mode 100644 packages/rocketchat-tokenpass/server/models/indexes.js delete mode 100644 packages/rocketchat-tokenpass/server/startup.js delete mode 100644 packages/rocketchat-tooltip/client/init.js delete mode 100644 packages/rocketchat-tooltip/package.js delete mode 100644 packages/rocketchat-tutum/package.js delete mode 100644 packages/rocketchat-tutum/startup.js delete mode 100644 packages/rocketchat-ui-account/client/account.js delete mode 100644 packages/rocketchat-ui-account/client/accountFlex.js delete mode 100644 packages/rocketchat-ui-account/client/accountPreferences.html delete mode 100644 packages/rocketchat-ui-account/client/accountPreferences.js delete mode 100644 packages/rocketchat-ui-account/client/avatar/avatar.js delete mode 100644 packages/rocketchat-ui-account/package.js delete mode 100644 packages/rocketchat-ui-admin/client/SettingsCachedCollection.js delete mode 100644 packages/rocketchat-ui-admin/client/admin.js delete mode 100644 packages/rocketchat-ui-admin/client/adminFlex.js delete mode 100644 packages/rocketchat-ui-admin/client/rooms/adminRooms.html delete mode 100644 packages/rocketchat-ui-admin/client/rooms/adminRooms.js delete mode 100644 packages/rocketchat-ui-admin/client/users/adminUserChannels.js delete mode 100644 packages/rocketchat-ui-admin/client/users/adminUserInfo.html delete mode 100644 packages/rocketchat-ui-admin/client/users/adminUsers.html delete mode 100644 packages/rocketchat-ui-admin/client/users/adminUsers.js delete mode 100644 packages/rocketchat-ui-admin/package.js delete mode 100644 packages/rocketchat-ui-admin/publications/adminRooms.js delete mode 100644 packages/rocketchat-ui-clean-history/client/lib/startup.js delete mode 100644 packages/rocketchat-ui-clean-history/package.js delete mode 100644 packages/rocketchat-ui-flextab/client/app.js delete mode 100644 packages/rocketchat-ui-flextab/client/flexTabBar.js delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/membersList.html delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/membersList.js delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/uploadedFilesList.js delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/userActions.js delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/userEdit.html delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/userEdit.js delete mode 100644 packages/rocketchat-ui-flextab/client/tabs/userInfo.js delete mode 100644 packages/rocketchat-ui-flextab/package.js delete mode 100644 packages/rocketchat-ui-login/client/login/form.html delete mode 100644 packages/rocketchat-ui-login/client/login/form.js delete mode 100644 packages/rocketchat-ui-login/client/login/header.js delete mode 100644 packages/rocketchat-ui-login/client/login/layout.js delete mode 100644 packages/rocketchat-ui-login/client/login/services.js delete mode 100644 packages/rocketchat-ui-login/client/routes.js delete mode 100644 packages/rocketchat-ui-login/client/username/layout.js delete mode 100644 packages/rocketchat-ui-login/package.js delete mode 100644 packages/rocketchat-ui-master/client/main.html delete mode 100644 packages/rocketchat-ui-master/client/main.js delete mode 100644 packages/rocketchat-ui-master/package.js delete mode 100644 packages/rocketchat-ui-master/public/icons.svg delete mode 100644 packages/rocketchat-ui-master/server/inject.js delete mode 100644 packages/rocketchat-ui-message/client/message.html delete mode 100644 packages/rocketchat-ui-message/client/message.js delete mode 100644 packages/rocketchat-ui-message/client/messageBox.html delete mode 100644 packages/rocketchat-ui-message/client/messageBox.js delete mode 100644 packages/rocketchat-ui-message/client/messageDropdown.html delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupChannel.js delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupConfig.html delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupConfig.js delete mode 100644 packages/rocketchat-ui-message/client/popup/messagePopupEmoji.js delete mode 100644 packages/rocketchat-ui-message/client/renderMessageBody.js delete mode 100644 packages/rocketchat-ui-message/package.js delete mode 100644 packages/rocketchat-ui-message/startup/messageBoxActions.js delete mode 100644 packages/rocketchat-ui-sidenav/client/chatRoomItem.js delete mode 100644 packages/rocketchat-ui-sidenav/client/createCombinedFlex.html delete mode 100644 packages/rocketchat-ui-sidenav/client/createCombinedFlex.js delete mode 100644 packages/rocketchat-ui-sidenav/client/listChannelsFlex.html delete mode 100644 packages/rocketchat-ui-sidenav/client/listChannelsFlex.js delete mode 100644 packages/rocketchat-ui-sidenav/client/listCombinedFlex.html delete mode 100644 packages/rocketchat-ui-sidenav/client/listCombinedFlex.js delete mode 100644 packages/rocketchat-ui-sidenav/client/listPrivateGroupsFlex.html delete mode 100644 packages/rocketchat-ui-sidenav/client/listPrivateGroupsFlex.js delete mode 100644 packages/rocketchat-ui-sidenav/client/roomList.js delete mode 100644 packages/rocketchat-ui-sidenav/client/sideNav.html delete mode 100644 packages/rocketchat-ui-sidenav/client/sideNav.js delete mode 100644 packages/rocketchat-ui-sidenav/client/sidebarHeader.js delete mode 100644 packages/rocketchat-ui-sidenav/client/sidebarItem.html delete mode 100644 packages/rocketchat-ui-sidenav/client/sidebarItem.js delete mode 100644 packages/rocketchat-ui-sidenav/client/sortlist.js delete mode 100644 packages/rocketchat-ui-sidenav/client/toolbar.js delete mode 100644 packages/rocketchat-ui-sidenav/package.js delete mode 100644 packages/rocketchat-ui-vrecord/client/VRecDialog.js delete mode 100644 packages/rocketchat-ui-vrecord/client/vrecord.js delete mode 100644 packages/rocketchat-ui-vrecord/package.js delete mode 100644 packages/rocketchat-ui-vrecord/server/settings.js delete mode 100644 packages/rocketchat-ui/client/components/contextualBar.html delete mode 100644 packages/rocketchat-ui/client/components/contextualBar.js delete mode 100644 packages/rocketchat-ui/client/components/header/header.html delete mode 100644 packages/rocketchat-ui/client/components/header/header.js delete mode 100644 packages/rocketchat-ui/client/components/icon.js delete mode 100644 packages/rocketchat-ui/client/components/popupList.html delete mode 100644 packages/rocketchat-ui/client/components/popupList.js delete mode 100644 packages/rocketchat-ui/client/components/selectDropdown.html delete mode 100644 packages/rocketchat-ui/client/components/selectDropdown.js delete mode 100644 packages/rocketchat-ui/client/components/tabs.html delete mode 100644 packages/rocketchat-ui/client/components/tabs.js delete mode 100644 packages/rocketchat-ui/client/lib/RoomHistoryManager.js delete mode 100644 packages/rocketchat-ui/client/lib/RoomManager.js delete mode 100644 packages/rocketchat-ui/client/lib/accountBox.js delete mode 100644 packages/rocketchat-ui/client/lib/accounts.js delete mode 100644 packages/rocketchat-ui/client/lib/avatar.js delete mode 100644 packages/rocketchat-ui/client/lib/chatMessages.js delete mode 100644 packages/rocketchat-ui/client/lib/collections.js delete mode 100644 packages/rocketchat-ui/client/lib/cordova/facebook-login.js delete mode 100644 packages/rocketchat-ui/client/lib/cordova/keyboard-fix.js delete mode 100644 packages/rocketchat-ui/client/lib/cordova/push.js delete mode 100644 packages/rocketchat-ui/client/lib/cordova/urls.js delete mode 100644 packages/rocketchat-ui/client/lib/cordova/user-state.js delete mode 100644 packages/rocketchat-ui/client/lib/esc.js delete mode 100644 packages/rocketchat-ui/client/lib/fireEvent.js delete mode 100644 packages/rocketchat-ui/client/lib/menu.js delete mode 100644 packages/rocketchat-ui/client/lib/modal.js delete mode 100644 packages/rocketchat-ui/client/lib/msgTyping.js delete mode 100644 packages/rocketchat-ui/client/lib/notification.js delete mode 100644 packages/rocketchat-ui/client/lib/recorderjs/audioRecorder.js delete mode 100644 packages/rocketchat-ui/client/lib/recorderjs/recorder.js delete mode 100644 packages/rocketchat-ui/client/lib/recorderjs/videoRecorder.js delete mode 100644 packages/rocketchat-ui/client/lib/sideNav.js delete mode 100644 packages/rocketchat-ui/client/lib/tapi18n.js delete mode 100644 packages/rocketchat-ui/client/lib/textarea-autogrow.js delete mode 100644 packages/rocketchat-ui/client/lib/textarea-cursor/set-cursor-position.js delete mode 100644 packages/rocketchat-ui/client/views/404/roomNotFound.js delete mode 100644 packages/rocketchat-ui/client/views/app/burger.js delete mode 100644 packages/rocketchat-ui/client/views/app/createChannel.js delete mode 100644 packages/rocketchat-ui/client/views/app/directory.html delete mode 100644 packages/rocketchat-ui/client/views/app/directory.js delete mode 100644 packages/rocketchat-ui/client/views/app/fullModal.js delete mode 100644 packages/rocketchat-ui/client/views/app/helpers.js delete mode 100644 packages/rocketchat-ui/client/views/app/home.js delete mode 100644 packages/rocketchat-ui/client/views/app/modal.html delete mode 100644 packages/rocketchat-ui/client/views/app/modal.js delete mode 100644 packages/rocketchat-ui/client/views/app/room.html delete mode 100644 packages/rocketchat-ui/client/views/app/room.js delete mode 100644 packages/rocketchat-ui/client/views/app/roomSearch.js delete mode 100644 packages/rocketchat-ui/client/views/app/secretURL.js delete mode 100644 packages/rocketchat-ui/client/views/app/videoCall/videoCall.html delete mode 100644 packages/rocketchat-ui/client/views/app/videoCall/videoCall.js delete mode 100644 packages/rocketchat-ui/client/views/cmsPage.html delete mode 100644 packages/rocketchat-ui/client/views/cmsPage.js delete mode 100644 packages/rocketchat-ui/client/views/modal.js delete mode 100644 packages/rocketchat-ui/getAvatarUrlFromUsername.js delete mode 100644 packages/rocketchat-ui/package.js delete mode 100644 packages/rocketchat-user-data-download/package.js delete mode 100644 packages/rocketchat-user-data-download/server/startup/settings.js delete mode 100644 packages/rocketchat-version-check/client/client.js delete mode 100644 packages/rocketchat-version-check/package.js delete mode 100644 packages/rocketchat-version-check/server/addSettings.js delete mode 100644 packages/rocketchat-version-check/server/checkUpdate.js delete mode 100644 packages/rocketchat-version-check/server/functions/checkVersionUpdate.js delete mode 100644 packages/rocketchat-version-check/server/functions/getNewUpdates.js delete mode 100644 packages/rocketchat-version-check/server/logger.js delete mode 100644 packages/rocketchat-version-check/server/methods/banner_dismiss.js delete mode 100644 packages/rocketchat-version-check/server/server.js delete mode 100644 packages/rocketchat-videobridge/client/actionLink.js delete mode 100644 packages/rocketchat-videobridge/client/tabBar.js delete mode 100644 packages/rocketchat-videobridge/client/views/videoFlexTab.js delete mode 100644 packages/rocketchat-videobridge/client/views/videoFlexTabBbb.js delete mode 100644 packages/rocketchat-videobridge/lib/messageType.js delete mode 100644 packages/rocketchat-videobridge/package.js delete mode 100644 packages/rocketchat-videobridge/server/actionLink.js delete mode 100644 packages/rocketchat-videobridge/server/methods/jitsiSetTimeout.js delete mode 100644 packages/rocketchat-videobridge/server/models/Rooms.js delete mode 100644 packages/rocketchat-videobridge/server/settings.js delete mode 100644 packages/rocketchat-webrtc/package.js delete mode 100644 packages/rocketchat-webrtc/server/settings.js delete mode 100644 packages/rocketchat-wordpress/common.js delete mode 100644 packages/rocketchat-wordpress/package.js delete mode 100644 packages/rocketchat-wordpress/startup.js delete mode 100755 packages/tap-i18n/.gitignore rename packages/{rocketchat-accounts/.npm/package => tap-i18n/.npm/plugin/tap-i18n-compiler}/.gitignore (100%) rename packages/{rocketchat-accounts/.npm/package => tap-i18n/.npm/plugin/tap-i18n-compiler}/README (100%) create mode 100644 packages/tap-i18n/.npm/plugin/tap-i18n-compiler/npm-shrinkwrap.json delete mode 100755 packages/tap-i18n/LICENSE delete mode 100755 packages/tap-i18n/README.md delete mode 100755 packages/tap-i18n/TODO delete mode 100755 packages/tap-i18n/lib/globals.js delete mode 100755 packages/tap-i18n/lib/plugin/compiler_configuration.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/i18n.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/i18n.generic_compiler.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/i18n.json.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/i18n.yml.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/package-tap.i18n.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/project-tap.i18n.coffee delete mode 100755 packages/tap-i18n/lib/plugin/compilers/share.coffee delete mode 100755 packages/tap-i18n/lib/plugin/etc/language_names.js delete mode 100755 packages/tap-i18n/lib/plugin/helpers/compile_step_helpers.coffee delete mode 100755 packages/tap-i18n/lib/plugin/helpers/helpers.coffee delete mode 100755 packages/tap-i18n/lib/plugin/helpers/load_json.coffee delete mode 100755 packages/tap-i18n/lib/plugin/helpers/load_yml.coffee delete mode 100755 packages/tap-i18n/lib/tap_i18n/tap_i18n-client.coffee delete mode 100755 packages/tap-i18n/lib/tap_i18n/tap_i18n-common.coffee delete mode 100755 packages/tap-i18n/lib/tap_i18n/tap_i18n-helpers.coffee delete mode 100755 packages/tap-i18n/lib/tap_i18n/tap_i18n-init.coffee delete mode 100755 packages/tap-i18n/lib/tap_i18n/tap_i18n-server.coffee delete mode 100755 packages/tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js delete mode 100755 packages/tap-i18n/lib/tap_i18next/tap_i18next_init.js delete mode 100755 packages/tap-i18n/package.js create mode 100644 private/client/imports/general/variables.css create mode 100644 private/public/icons.svg rename {packages/chatpal-search => private}/server/asset/chatpal-enter.svg (100%) rename {packages/chatpal-search => private}/server/asset/chatpal-logo-icon-darkblue.svg (100%) rename {packages/rocketchat-theme => private}/server/colors.less (92%) rename {packages/rocketchat-ui-master => private}/server/dynamic-css.js (95%) rename {packages/rocketchat-theme => public}/client/vendor/fontello/demo.html (100%) rename {packages/rocketchat-theme => public}/client/vendor/fontello/utf8-rtl.html (100%) rename {packages/rocketchat-theme/client/vendor/fontello => public}/font/fontello.eot (100%) rename {packages/rocketchat-theme/client/vendor/fontello => public}/font/fontello.svg (100%) rename {packages/rocketchat-theme/client/vendor/fontello => public}/font/fontello.ttf (100%) rename {packages/rocketchat-theme/client/vendor/fontello => public}/font/fontello.woff (100%) rename {packages/rocketchat-theme/client/vendor/fontello => public}/font/fontello.woff2 (100%) create mode 100644 public/fonts/KaTeX_AMS-Regular.ttf create mode 100644 public/fonts/KaTeX_AMS-Regular.woff create mode 100644 public/fonts/KaTeX_AMS-Regular.woff2 create mode 100644 public/fonts/KaTeX_Caligraphic-Bold.ttf create mode 100644 public/fonts/KaTeX_Caligraphic-Bold.woff create mode 100644 public/fonts/KaTeX_Caligraphic-Bold.woff2 create mode 100644 public/fonts/KaTeX_Caligraphic-Regular.ttf create mode 100644 public/fonts/KaTeX_Caligraphic-Regular.woff create mode 100644 public/fonts/KaTeX_Caligraphic-Regular.woff2 create mode 100644 public/fonts/KaTeX_Fraktur-Bold.ttf create mode 100644 public/fonts/KaTeX_Fraktur-Bold.woff create mode 100644 public/fonts/KaTeX_Fraktur-Bold.woff2 create mode 100644 public/fonts/KaTeX_Fraktur-Regular.ttf create mode 100644 public/fonts/KaTeX_Fraktur-Regular.woff create mode 100644 public/fonts/KaTeX_Fraktur-Regular.woff2 create mode 100644 public/fonts/KaTeX_Main-Bold.ttf create mode 100644 public/fonts/KaTeX_Main-Bold.woff create mode 100644 public/fonts/KaTeX_Main-Bold.woff2 create mode 100644 public/fonts/KaTeX_Main-BoldItalic.ttf create mode 100644 public/fonts/KaTeX_Main-BoldItalic.woff create mode 100644 public/fonts/KaTeX_Main-BoldItalic.woff2 create mode 100644 public/fonts/KaTeX_Main-Italic.ttf create mode 100644 public/fonts/KaTeX_Main-Italic.woff create mode 100644 public/fonts/KaTeX_Main-Italic.woff2 create mode 100644 public/fonts/KaTeX_Main-Regular.ttf create mode 100644 public/fonts/KaTeX_Main-Regular.woff create mode 100644 public/fonts/KaTeX_Main-Regular.woff2 create mode 100644 public/fonts/KaTeX_Math-Italic.ttf create mode 100644 public/fonts/KaTeX_Math-Italic.woff create mode 100644 public/fonts/KaTeX_Math-Italic.woff2 create mode 100644 public/fonts/KaTeX_SansSerif-Bold.ttf create mode 100644 public/fonts/KaTeX_SansSerif-Bold.woff create mode 100644 public/fonts/KaTeX_SansSerif-Bold.woff2 create mode 100644 public/fonts/KaTeX_SansSerif-Italic.ttf create mode 100644 public/fonts/KaTeX_SansSerif-Italic.woff create mode 100644 public/fonts/KaTeX_SansSerif-Italic.woff2 create mode 100644 public/fonts/KaTeX_SansSerif-Regular.ttf create mode 100644 public/fonts/KaTeX_SansSerif-Regular.woff create mode 100644 public/fonts/KaTeX_SansSerif-Regular.woff2 create mode 100644 public/fonts/KaTeX_Script-Regular.ttf create mode 100644 public/fonts/KaTeX_Script-Regular.woff create mode 100644 public/fonts/KaTeX_Script-Regular.woff2 create mode 100644 public/fonts/KaTeX_Size1-Regular.ttf create mode 100644 public/fonts/KaTeX_Size1-Regular.woff create mode 100644 public/fonts/KaTeX_Size1-Regular.woff2 create mode 100644 public/fonts/KaTeX_Size2-Regular.ttf create mode 100644 public/fonts/KaTeX_Size2-Regular.woff create mode 100644 public/fonts/KaTeX_Size2-Regular.woff2 create mode 100644 public/fonts/KaTeX_Size3-Regular.ttf create mode 100644 public/fonts/KaTeX_Size3-Regular.woff create mode 100644 public/fonts/KaTeX_Size3-Regular.woff2 create mode 100644 public/fonts/KaTeX_Size4-Regular.ttf create mode 100644 public/fonts/KaTeX_Size4-Regular.woff create mode 100644 public/fonts/KaTeX_Size4-Regular.woff2 create mode 100644 public/fonts/KaTeX_Typewriter-Regular.ttf create mode 100644 public/fonts/KaTeX_Typewriter-Regular.woff create mode 100644 public/fonts/KaTeX_Typewriter-Regular.woff2 create mode 100644 public/images/404.svg create mode 100644 public/images/logo/logo.png create mode 100644 public/packages/emojione/activity-sprites.png create mode 100644 public/packages/emojione/flags-sprites.png create mode 100644 public/packages/emojione/food-sprites.png create mode 100644 public/packages/emojione/modifier-sprites.png create mode 100644 public/packages/emojione/nature-sprites.png create mode 100644 public/packages/emojione/objects-sprites.png create mode 100644 public/packages/emojione/people-sprites.png create mode 100644 public/packages/emojione/regional-sprites.png create mode 100644 public/packages/emojione/symbols-sprites.png create mode 100644 public/packages/emojione/travel-sprites.png rename {packages/rocketchat-videobridge => public/packages/rocketchat_videobridge}/client/public/external_api.js (100%) create mode 100644 public/pdf.worker.min.js create mode 100644 public/public/icons.html create mode 100644 server/importPackages.js delete mode 100644 server/lib/cordova/facebook-login.js create mode 100644 server/methods/getRoomById.js delete mode 100644 server/methods/getUsernameSuggestion.js delete mode 100644 server/publications/room.js create mode 100644 server/publications/room/emitter.js create mode 100644 server/publications/room/index.js create mode 100644 server/publications/settings/emitter.js create mode 100644 server/publications/settings/index.js delete mode 100644 server/publications/subscription.js create mode 100644 server/publications/subscription/emitter.js create mode 100644 server/publications/subscription/index.js create mode 100644 server/routes/avatar/index.js create mode 100644 server/routes/avatar/middlewares/auth.js create mode 100644 server/routes/avatar/middlewares/index.js create mode 100644 server/routes/avatar/room.js create mode 100644 server/routes/avatar/user.js create mode 100644 server/routes/avatar/utils.js delete mode 100644 server/startup/avatar.js delete mode 100644 server/startup/i18n-validation.js create mode 100644 server/startup/migrations/index.js create mode 100644 server/startup/migrations/v133.js create mode 100644 server/startup/migrations/v134.js create mode 100644 server/startup/migrations/v135.js create mode 100644 server/startup/migrations/v136.js create mode 100644 server/startup/migrations/v137.js create mode 100644 server/startup/migrations/v138.js create mode 100644 server/startup/migrations/v139.js create mode 100644 server/startup/migrations/v140.js create mode 100644 server/startup/migrations/v141.js create mode 100644 server/startup/migrations/v142.js create mode 100644 server/startup/migrations/v143.js create mode 100644 server/startup/migrations/v144.js create mode 100644 server/startup/migrations/v145.js create mode 100644 server/startup/migrations/v146.js create mode 100644 server/startup/migrations/v147.js create mode 100644 server/startup/migrations/v148.js create mode 100644 server/startup/migrations/v149.js delete mode 100644 server/stream/messages.js create mode 100644 server/stream/messages/emitter.js create mode 100644 server/stream/messages/index.js create mode 100644 tests/.eslintrc create mode 100644 tests/data/chat.helper.js create mode 100644 tests/data/permissions.helper.js create mode 100644 tests/data/rooms.helper.js create mode 100644 tests/data/users.helper.js create mode 100644 tests/end-to-end/api/15-video-conference.js create mode 100644 tests/end-to-end/ui/15-discussion.js create mode 100644 tests/pageobjects/discussion.page.js create mode 100644 tests/pageobjects/keyboard.js create mode 100644 tests/pageobjects/settings.js diff --git a/.circleci/config.yml b/.circleci/config.yml index c20b34e4e789..22531096ccc9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,23 +1,86 @@ defaults: &defaults working_directory: ~/repo +attach_workspace: &attach_workspace + at: /tmp + +test-install-dependencies: &test-install-dependencies + name: Install dependencies + command: | + wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 + echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google.list + echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/4.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-4.0.list + sudo apt-get update + sudo apt-get install -y mongodb-org-shell google-chrome-stable + +test-run: &test-run + name: Run Tests + command: | + for i in $(seq 1 5); do mongo rocketchat --eval 'db.dropDatabase()' && npm test && s=0 && break || s=$? && sleep 1; done; (exit $s) + +test-npm-install: &test-npm-install + name: NPM install + command: | + npm install + +test-store_artifacts: &test-store_artifacts + path: .screenshots/ + +test-configure-replicaset: &test-configure-replicaset + name: Configure Replica Set + command: | + mongo --eval 'rs.initiate({_id:"rs0", members: [{"_id":1, "host":"localhost:27017"}]})' + mongo --eval 'rs.status()' + +test-restore-npm-cache: &test-restore-npm-cache + keys: + - node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} + +test-save-npm-cache: &test-save-npm-cache + key: node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} + paths: + - ./node_modules + +test-docker-image: &test-docker-image + circleci/node:8.11-stretch-browsers + +test-with-oplog: &test-with-oplog + <<: *defaults + environment: + TEST_MODE: "true" + MONGO_URL: mongodb://localhost:27017/rocketchat + MONGO_OPLOG_URL: mongodb://localhost:27017/local + + steps: + - attach_workspace: *attach_workspace + - checkout + - run: *test-install-dependencies + - run: *test-configure-replicaset + - restore_cache: *test-restore-npm-cache + - run: *test-npm-install + - run: *test-run + - save_cache: *test-save-npm-cache + - store_artifacts: *test-store_artifacts + version: 2 jobs: build: <<: *defaults docker: - - image: circleci/node:8.9 + - image: circleci/node:8.11-stretch + - image: mongo:3.2 steps: - checkout - # - restore_cache: - # keys: - # - node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} + - restore_cache: + keys: + - node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} - # - restore_cache: - # keys: - # - meteor-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/release" }} + - restore_cache: + keys: + - meteor-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/release" }} - run: name: Install Meteor @@ -64,28 +127,36 @@ jobs: - run: name: Unit Test command: | - meteor npm run testunit + MONGO_URL=mongodb://localhost:27017 meteor npm run testunit - # - restore_cache: - # keys: - # - meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/versions" }} + - restore_cache: + keys: + - meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/versions" }} - # - restore_cache: - # keys: - # - livechat-meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/app/.meteor/versions" }} + - restore_cache: + keys: + - livechat-meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/.app/.meteor/versions" }} - # - restore_cache: - # keys: - # - livechat-node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/app/package.json" }} + - restore_cache: + keys: + - livechat-node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/.app/package.json" }} - run: name: Build Rocket.Chat + environment: + TOOL_NODE_FLAGS: --max_old_space_size=3072 command: | - if [[ $CIRCLE_TAG ]]; then meteor reset; fi - set +e - meteor add rocketchat:lib - set -e - meteor build --server-only --directory /tmp/build-test + if [[ $CIRCLE_TAG ]] || [[ $CIRCLE_BRANCH == 'develop' ]]; then + meteor reset; + fi + + export CIRCLE_PR_NUMBER="${CIRCLE_PR_NUMBER:-${CIRCLE_PULL_REQUEST##*/}}" + if [[ -z $CIRCLE_PR_NUMBER ]]; then + meteor build --server-only --directory /tmp/build-test + else + export METEOR_PROFILE=1000 + meteor build --server-only --directory --debug /tmp/build-test + fi; - run: name: Prepare build @@ -96,30 +167,30 @@ jobs: cd /tmp/build-test/bundle/programs/server npm install - # - save_cache: - # key: node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} - # paths: - # - ./node_modules + - save_cache: + key: node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} + paths: + - ./node_modules - # - save_cache: - # key: meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/versions" }} - # paths: - # - ./.meteor/local + - save_cache: + key: meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/versions" }} + paths: + - ./.meteor/local - # - save_cache: - # key: livechat-node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/app/package.json" }} - # paths: - # - ./packages/rocketchat-livechat/app/node_modules + - save_cache: + key: livechat-node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/.app/package.json" }} + paths: + - ./packages/rocketchat-livechat/app/node_modules - # - save_cache: - # key: livechat-meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/app/.meteor/versions" }} - # paths: - # - ./packages/rocketchat-livechat/app/.meteor/local + - save_cache: + key: livechat-meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/.app/.meteor/versions" }} + paths: + - ./packages/rocketchat-livechat/app/.meteor/local - # - save_cache: - # key: meteor-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/release" }} - # paths: - # - ~/.meteor + - save_cache: + key: meteor-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/release" }} + paths: + - ~/.meteor - persist_to_workspace: root: /tmp/ @@ -130,94 +201,39 @@ jobs: - store_artifacts: path: /tmp/build - test-with-oplog: - <<: *defaults - docker: - - image: circleci/node:8.9-browsers - - image: mongo:3.4 - command: [mongod, --nojournal, --noprealloc, --smallfiles, --replSet=rs0] - - environment: - TEST_MODE: "true" - MONGO_URL: mongodb://localhost:27017/testwithoplog - MONGO_OPLOG_URL: mongodb://localhost:27017/local - - steps: - - attach_workspace: - at: /tmp - - - checkout - - - run: - name: Install dependencies - command: | - wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6 - echo "deb [ arch=amd64 ] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google.list - echo "deb [ arch=amd64 ] http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list - sudo apt-get update - sudo apt-get install -y mongodb-org-shell google-chrome-stable - - - run: - name: Configure Replica Set - command: | - mongo --eval 'rs.initiate({_id:"rs0", members: [{"_id":1, "host":"localhost:27017"}]})' - mongo --eval 'rs.status()' - - - run: - name: NPM install - command: | - npm install - - - run: - name: Run Tests - command: | - for i in $(seq 1 5); do npm test && s=0 && break || s=$? && sleep 1; done; (exit $s) - - store_artifacts: - path: .screenshots/ - - test-without-oplog: - <<: *defaults + test-with-oplog-mongo-3-2: + <<: *test-with-oplog docker: - - image: circleci/node:8.9-browsers - - image: circleci/mongo:3.4 - - environment: - TEST_MODE: "true" - MONGO_URL: mongodb://localhost:27017/testwithoplog - - steps: - - attach_workspace: - at: /tmp - - - checkout - - - run: - name: Install dependencies - command: | - wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - echo "deb [ arch=amd64 ] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google.list - sudo apt-get update - sudo apt-get install -y google-chrome-stable + - image: *test-docker-image + - image: mongo:3.2 + command: [mongod, --noprealloc, --smallfiles, --replSet=rs0] - - run: - name: NPM install - command: | - npm install + test-with-oplog-mongo-3-4: + <<: *test-with-oplog + docker: + - image: *test-docker-image + - image: mongo:3.4 + command: [mongod, --noprealloc, --smallfiles, --replSet=rs0] - - run: - name: Run Tests - command: | - for i in $(seq 1 5); do npm test && s=0 && break || s=$? && sleep 1; done; (exit $s) + test-with-oplog-mongo-3-6: + <<: *test-with-oplog + docker: + - image: *test-docker-image + - image: mongo:3.6 + command: [mongod, --noprealloc, --smallfiles, --replSet=rs0] - - store_artifacts: - path: .screenshots/ + test-with-oplog-mongo-4-0: + <<: *test-with-oplog + docker: + - image: *test-docker-image + - image: mongo:4.0 + command: [mongod, --noprealloc, --smallfiles, --replSet=rs0] deploy: <<: *defaults docker: - - image: circleci/node:8.9 + - image: circleci/node:8.11-stretch steps: - attach_workspace: @@ -231,9 +247,9 @@ jobs: if [[ $CIRCLE_PULL_REQUESTS ]]; then exit 0; fi; sudo apt-get -y -qq update - sudo apt-get -y -qq install python3.4-dev + sudo apt-get -y -qq install python3.5-dev curl -O https://bootstrap.pypa.io/get-pip.py - python3.4 get-pip.py --user + python3.5 get-pip.py --user export PATH=~/.local/bin:$PATH pip install awscli --upgrade --user @@ -249,7 +265,6 @@ jobs: source .circleci/setdeploydir.sh bash .circleci/setupsig.sh bash .circleci/namefiles.sh - # echo ".circleci/sandstorm.sh" aws s3 cp $ROCKET_DEPLOY_DIR/ s3://download.rocket.chat/build/ --recursive @@ -327,6 +342,87 @@ jobs: exit 0 fi; + pr-build: + <<: *defaults + docker: + - image: circleci/node:8.11-stretch + + steps: + - checkout + + - restore_cache: + keys: + - node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "package.json" }} + + - restore_cache: + keys: + - meteor-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/release" }} + + - run: + name: Install Meteor + command: | + # Restore bin from cache + set +e + METEOR_SYMLINK_TARGET=$(readlink ~/.meteor/meteor) + METEOR_TOOL_DIRECTORY=$(dirname "$METEOR_SYMLINK_TARGET") + set -e + LAUNCHER=$HOME/.meteor/$METEOR_TOOL_DIRECTORY/scripts/admin/launch-meteor + if [ -e $LAUNCHER ] + then + echo "Cached Meteor bin found, restoring it" + sudo cp "$LAUNCHER" "/usr/local/bin/meteor" + else + echo "No cached Meteor bin found." + fi + + # only install meteor if bin isn't found + command -v meteor >/dev/null 2>&1 || curl https://install.meteor.com | sed s/--progress-bar/-sL/g | /bin/sh + + - run: + name: Versions + command: | + npm --versions + node -v + meteor --version + meteor npm --versions + meteor node -v + git version + + - run: + name: Meteor npm install + command: | + # rm -rf node_modules + # rm -f package-lock.json + meteor npm install + + - restore_cache: + keys: + - meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum ".meteor/versions" }} + + - restore_cache: + keys: + - livechat-meteor-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/.app/.meteor/versions" }} + + - restore_cache: + keys: + - livechat-node-modules-cache-{{ checksum ".circleci/config.yml" }}-{{ checksum "packages/rocketchat-livechat/.app/package.json" }} + + - run: + name: Build Rocket.Chat + environment: + TOOL_NODE_FLAGS: --max_old_space_size=3072 + command: | + meteor build --server-only /tmp/build-pr + + + - persist_to_workspace: + root: /tmp/ + paths: + - build-pr + + - store_artifacts: + path: /tmp/build-pr + pr-image-build: <<: *defaults @@ -349,9 +445,9 @@ jobs: exit 0 fi; - cd /tmp/build - tar xzf Rocket.Chat.tar.gz - rm Rocket.Chat.tar.gz + cd /tmp/build-pr + tar xzf repo.tar.gz + rm repo.tar.gz docker login -u $DOCKER_USER -p $DOCKER_PASS @@ -360,11 +456,11 @@ jobs: docker build -t rocketchat/rocket.chat:pr-$CIRCLE_PR_NUMBER . docker push rocketchat/rocket.chat:pr-$CIRCLE_PR_NUMBER - echo "Build preview Docker image" - cp ~/repo/.docker-mongo/Dockerfile . - cp ~/repo/.docker-mongo/entrypoint.sh . - docker build -t rocketchat/rocket.chat.preview:pr-$CIRCLE_PR_NUMBER . - docker push rocketchat/rocket.chat.preview:pr-$CIRCLE_PR_NUMBER + #echo "Build preview Docker image" + #cp ~/repo/.docker-mongo/Dockerfile . + #cp ~/repo/.docker-mongo/entrypoint.sh . + #docker build -t rocketchat/rocket.chat.preview:pr-$CIRCLE_PR_NUMBER . + #docker push rocketchat/rocket.chat.preview:pr-$CIRCLE_PR_NUMBER workflows: version: 2 @@ -373,28 +469,34 @@ workflows: - build: filters: tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ - - test-with-oplog: + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ + - test-with-oplog-mongo-3-2: &test-mongo requires: - build filters: tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ - - test-without-oplog: + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ + - test-with-oplog-mongo-3-4: &test-mongo-no-pr requires: - build filters: + branches: + only: develop tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ + - test-with-oplog-mongo-3-6: *test-mongo-no-pr + - test-with-oplog-mongo-4-0: *test-mongo - deploy: requires: - - test-with-oplog - - test-without-oplog + - test-with-oplog-mongo-3-2 + - test-with-oplog-mongo-3-4 + - test-with-oplog-mongo-3-6 + - test-with-oplog-mongo-4-0 filters: branches: only: develop tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ - image-build: requires: - deploy @@ -402,7 +504,7 @@ workflows: branches: only: develop tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ - hold: type: approval requires: @@ -411,13 +513,21 @@ workflows: branches: ignore: develop tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ - - pr-image-build: + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ + - pr-build: requires: - hold filters: branches: ignore: develop tags: - only: /^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ + - pr-image-build: + requires: + - pr-build + filters: + branches: + ignore: develop + tags: + only: /^[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|beta)\.[0-9]+)?$/ diff --git a/.circleci/setartname.sh b/.circleci/setartname.sh index e61fd52f1a41..acfdb3e032e6 100644 --- a/.circleci/setartname.sh +++ b/.circleci/setartname.sh @@ -15,7 +15,7 @@ elif [[ $CIRCLE_TAG =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then else SNAP_CHANNEL=edge RC_RELEASE=develop - RC_VERSION=0.66.0-develop + RC_VERSION="$(npm run version --silent)" fi export SNAP_CHANNEL 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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 buildinfo # Clone launchpad repo for the channel down. -git clone -b $SNAP_CHANNEL git+ssh://rocket.chat.buildmaster@git.launchpad.net/rocket.chat launchpad +git clone -b $SNAP_CHANNEL --depth 1 git+ssh://rocket.chat.buildmaster@git.launchpad.net/rocket.chat launchpad # Rarely will change, but just incase we copy it all -cp -r resources buildinfo launchpad/ -sed s/#{RC_VERSION}/$RC_VERSION/ snapcraft.yaml > launchpad/snapcraft.yaml +cp -r resources buildinfo snap launchpad/ +sed s/#{RC_VERSION}/$RC_VERSION/ snap/snapcraft.yaml > launchpad/snap/snapcraft.yaml sed s/#{RC_VERSION}/$RC_VERSION/ resources/prepareRocketChat > launchpad/resources/prepareRocketChat cd launchpad -git add resources snapcraft.yaml buildinfo +git add resources snap buildinfo # Set commit author details git config user.email "buildmaster@rocket.chat" diff --git a/.circleci/update-releases.sh b/.circleci/update-releases.sh index 4e133c304fed..3d831115b441 100644 --- a/.circleci/update-releases.sh +++ b/.circleci/update-releases.sh @@ -5,3 +5,6 @@ IFS=$'\n\t' curl -X POST \ -H "X-Update-Token: ${UPDATE_TOKEN}" \ https://releases.rocket.chat/update + +# Makes build fail if the release isn't there +curl --fail https://releases.rocket.chat/$RC_VERSION/info diff --git a/.docker-mongo/Dockerfile b/.docker-mongo/Dockerfile index 8a5faf28da37..bc250d596713 100644 --- a/.docker-mongo/Dockerfile +++ b/.docker-mongo/Dockerfile @@ -6,8 +6,8 @@ ADD entrypoint.sh /app/bundle/ MAINTAINER buildmaster@rocket.chat RUN set -x \ - && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 \ - && echo "deb http://repo.mongodb.org/apt/debian jessie/mongodb-org/3.6 main" | tee /etc/apt/sources.list.d/mongodb-org-3.6.list \ + && apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 9DA31620334BD75D9DCB49F368818C72E52529D4 \ + && echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/4.0 main" | tee /etc/apt/sources.list.d/mongodb-org-4.0.list \ && apt-get update \ && apt-get install -y --force-yes pwgen mongodb-org \ && echo "mongodb-org hold" | dpkg --set-selections \ diff --git a/.docker/Dockerfile.local b/.docker/Dockerfile.local deleted file mode 100644 index 9fc3eb43797b..000000000000 --- a/.docker/Dockerfile.local +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:8 - -ADD . /app - -ENV RC_VERSION=0.57.0-designpreview \ - DEPLOY_METHOD=docker \ - NODE_ENV=production \ - PORT=3000 \ - ROOT_URL=http://localhost:3000 - -RUN set -x \ - && cd /app/bundle/programs/server \ - && npm install \ - && npm cache clear --force - -WORKDIR /app/bundle - -EXPOSE 3000 - -CMD ["node", "main.js"] diff --git a/.docker/Dockerfile.rhel b/.docker/Dockerfile.rhel index 264b70bd007a..d82ace7a223a 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 0.70.0-develop +ENV RC_VERSION 1.4.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/.eslintignore b/.eslintignore index f8e66c28c01d..0f9a4f4d54d5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,23 +2,24 @@ node_modules packages/autoupdate/ packages/meteor-streams/ packages/meteor-timesync/ -packages/rocketchat-emoji-emojione/generateEmojiIndex.js -packages/rocketchat-favico/favico.js -packages/rocketchat-katex/client/katex/katex.min.js +app/emoji-emojione/generateEmojiIndex.js +app/favico/favico.js +app/katex/client/katex/katex.min.js packages/rocketchat-livechat/.app/node_modules packages/rocketchat-livechat/.app/.meteor packages/rocketchat-livechat/assets/rocketchat-livechat.min.js packages/rocketchat-livechat/assets/rocket-livechat.js -packages/rocketchat-theme/client/minicolors/jquery.minicolors.js -packages/rocketchat-ui/client/lib/customEventPolyfill.js -packages/rocketchat-ui/client/lib/Modernizr.js -packages/rocketchat-ui/client/lib/recorderjs/recorder.js -packages/rocketchat-videobridge/client/public/external_api.js -packages/rocketchat-theme/client/vendor/ +app/theme/client/minicolors/jquery.minicolors.js +app/theme/client/vendor/ +app/ui/client/lib/customEventPolyfill.js +app/ui/client/lib/Modernizr.js +public/mp3-realtime-worker.js +public/lame.min.js +public/packages/rocketchat_videobridge/client/public/external_api.js packages/tap-i18n/lib/tap_i18next/tap_i18next-1.7.3.js private/moment-locales/ public/livechat/ -public/mp3-realtime-worker.js -public/lame.min.js !.scripts !packages/rocketchat-livechat/.app +public/pdf.worker.min.js +imports/client/ diff --git a/.eslintrc b/.eslintrc index 7a4aeb6d2fd6..74f9766d2044 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,74 +1,11 @@ { "extends": ["@rocket.chat/eslint-config"], + "parser": "babel-eslint", "globals": { + "__meteor_bootstrap__" : false, "__meteor_runtime_config__" : false, - "AccountBox" : false, - "Accounts" : false, - "AgentUsers" : false, - "Apps" : false, "Assets" : false, - "Blaze" : false, - "BlazeLayout" : false, - "browser" : false, - "ChatMessage" : false, - "ChatMessages" : false, - "ChatRoom" : false, - "ChatSubscription" : false, - "check" : false, - "CryptoJS" : false, - "Department" : false, - "DDPRateLimiter" : false, - "EJSON" : false, - "Email" : false, - "FlowRouter" : false, - "FileUpload" : false, - "HTTP" : false, - "getNextAgent" : false, - "handleError" : false, - "getAvatarUrlFromUsername" : false, - "LivechatCustomField" : false, - "LivechatDepartment" : false, - "LivechatDepartmentAgents" : false, - "livechatManagerRoutes" : true, - "LivechatMonitoring" : false, - "LivechatPageVisited" : false, - "LivechatTrigger" : false, - "Logger" : false, - "Match" : false, - "Meteor" : false, - "modal" : false, - "moment" : false, - "Mongo" : false, - "Npm" : false, - "Package" : false, - "parentCall" : false, - "Promise" : false, - "Random" : false, - "ReactiveDict" : false, - "ReactiveVar" : false, - "Reload" : false, - "RocketChat" : true, - "RocketChatFile" : false, - "RoomHistoryManager" : false, - "RoomManager" : false, - "ServiceConfiguration" : false, - "Session" : false, - "Settings" : false, - "SHA256" : false, - "SideNav" : false, - "t" : false, - "TAPi18n" : false, - "TAPi18next" : false, - "Template" : false, - "TimeSync" : false, - "toastr" : false, - "Tracker" : false, - "Trigger" : false, - "Triggers" : false, - "UAParser" : false, - "visitor" : false, - "WebApp" : false, - "VideoRecorder" : false, - "VRecDialog" : false + "chrome" : false, + "jscolor" : false } } diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 066b2d920a28..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/bot-config.yml b/.github/bot-config.yml index 2d181373504f..8df57f4e966d 100644 --- a/.github/bot-config.yml +++ b/.github/bot-config.yml @@ -5,6 +5,7 @@ whitelist: - TwizzyDizzy - theorenck - JSzaszvari + - reetp labels: - "Contributions: welcome" - "Contributions: only core team" diff --git a/.github/changelog.js b/.github/changelog.js deleted file mode 100644 index 438d194d23ab..000000000000 --- a/.github/changelog.js +++ /dev/null @@ -1,116 +0,0 @@ -/* eslint no-var: 0, object-shorthand: 0, prefer-template: 0 */ - -'use strict'; -var readFile = require('fs').readFileSync; -var resolve = require('path').resolve; -var gitUrl = 'https://github.com/RocketChat/Rocket.Chat'; - -var parserOpts = { - headerPattern: /^(\[([A-z]+)\] )?(.*)$/m, - headerCorrespondence: [ - 'stype', - 'type', - 'subject' - ], - mergePattern: /^Merge pull request #(.*) from .*$/, - mergeCorrespondence: ['pr'] - // noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES'], - // revertPattern: /^revert:\s([\s\S]*?)\s*This reverts commit (\w*)\./, - // revertCorrespondence: ['header', 'hash'], - // mergePattern: /^Merge pull request #(\d+) from (.*)$/, - // mergeCorrespondence: ['id', 'source'] -}; - -var LABELS = { - BREAK: { - title: 'BREAKING CHANGES', - collapse: false - }, - NEW: { - title: 'New Features', - collapse: false - }, - FIX: { - title: 'Bug Fixes', - collapse: false - }, - DOC: { - title: 'Documentation', - collapse: true - }, - OTHER: { - title: 'Others', - collapse: true - } -}; - -var sort = Object.keys(LABELS); - -var writerOpts = { - transform: function(commit) { - if (!commit.pr) { - return; - } - - // console.log(commit); - commit.type = (commit.type || 'OTHER').toUpperCase(); - if (LABELS[commit.type] == null) { - return; - } - - commit.pr_url = gitUrl + '/pull/' + commit.pr; - - var issues = []; - - if (typeof commit.hash === 'string') { - commit.hash = commit.hash.substring(0, 7); - } - - if (typeof commit.subject === 'string') { - // GitHub issue URLs. - commit.subject = commit.subject.replace(/#([0-9]+)/g, function(_, issue) { - issues.push(issue); - return '[#' + issue + '](' + gitUrl + '/issues/' + issue + ')'; - }); - // GitHub user URLs. - commit.subject = commit.subject.replace(/@([a-zA-Z0-9_]+)/g, '[@$1](https://github.com/$1)'); - } - - // remove references that already appear in the subject - commit.references = commit.references.filter(function(reference) { - if (issues.indexOf(reference.issue) === -1) { - return true; - } - - return false; - }); - - return commit; - }, - groupBy: 'type', - commitGroupsSort: function(a, b) { - return sort.indexOf(a.title) > sort.indexOf(b.title); - }, - finalizeContext: function(context) { - context.commitGroups.forEach(function(group) { - Object.assign(group, LABELS[group.title.toUpperCase()]); - }); - - // console.log(context); - return context; - }, - commitsSort: ['subject'] -}; - -writerOpts.mainTemplate = readFile(resolve(__dirname, 'templates/template.hbs'), 'utf-8'); -writerOpts.headerPartial = readFile(resolve(__dirname, 'templates/header.hbs'), 'utf-8'); -writerOpts.commitPartial = readFile(resolve(__dirname, 'templates/commit.hbs'), 'utf-8'); -writerOpts.footerPartial = readFile(resolve(__dirname, 'templates/footer.hbs'), 'utf-8'); - -module.exports = { - gitRawCommitsOpts: { - merges: null - }, - parserOpts: parserOpts, - writerOpts: writerOpts -}; diff --git a/.github/history-manual.json b/.github/history-manual.json index 394af77f43a0..d3783a7b48e2 100644 --- a/.github/history-manual.json +++ b/.github/history-manual.json @@ -11,5 +11,10 @@ "title": "[FIX] Removed Deprecated Package rocketchat:sharedsecret`", "userLogin": "rodrigok", "contributors": [] + }], + "0.72.0-rc.0": [{ + "title": "[BREAK] Support for Cordova (Rocket.Chat Legacy app) has reached End-of-life, support has been discontinued", + "userLogin": "sampaiodiego", + "contributors": [] }] } diff --git a/.github/history.json b/.github/history.json index b0b510da35de..b7ca1acf8b06 100644 --- a/.github/history.json +++ b/.github/history.json @@ -18679,6 +18679,14787 @@ ] } ] + }, + "0.69.2": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "11812", + "title": "[NEW] Include room name in stream for bots", + "userLogin": "timkinnane", + "milestone": "0.69.2", + "contributors": [ + "timkinnane", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12022", + "title": "[FIX] Reset password link error if already logged in", + "userLogin": "rodrigok", + "milestone": "0.69.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11992", + "title": "[FIX] Apps: setting with 'code' type only saving last line", + "userLogin": "cardoso", + "milestone": "0.69.2", + "contributors": [ + "cardoso" + ] + }, + { + "pr": "11955", + "title": "[FIX] Update user information not possible by admin if disabled to users", + "userLogin": "kaiiiiiiiii", + "milestone": "0.69.2", + "contributors": [ + "kaiiiiiiiii" + ] + }, + { + "pr": "12025", + "title": "[FIX] Hidden admin sidenav on embedded layout", + "userLogin": "ggazzo", + "milestone": "0.69.2", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "0.70.0-rc.0": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "11184", + "title": "[NEW] Allow multiple subcommands in MIGRATION_VERSION env variable", + "userLogin": "arch119", + "contributors": [ + null, + "arch119", + "web-flow" + ] + }, + { + "pr": "10094", + "title": "[NEW] Support for end to end encryption", + "userLogin": "mrinaldhar", + "milestone": "Short-term", + "contributors": [ + "mrinaldhar" + ] + }, + { + "pr": "11936", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12083", + "title": "[IMPROVE] Cache livechat get agent trigger call", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12097", + "title": "[FIX] Livechat agent joining on pick from guest pool", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson" + ] + }, + { + "pr": "12107", + "title": "[IMPROVE] BigBlueButton joinViaHtml5 and video icon on sidebar", + "userLogin": "ggazzo", + "milestone": "0.70.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "11238", + "title": "[NEW] Livechat Analytics and Reports", + "userLogin": "pkgodara", + "milestone": "0.70.0", + "contributors": [ + "pkgodara", + "web-flow" + ] + }, + { + "pr": "12115", + "title": "Better organize package.json", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12114", + "title": "Fix using wrong variable", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "11993", + "title": "[NEW] Apps: Add handlers for message updates", + "userLogin": "cardoso", + "contributors": [ + "cardoso", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "11780", + "title": "[FIX] Apps: Add missing reactions and actions properties to app message object", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "10588", + "title": "[NEW] Livechat notifications on new incoming inquiries for guest-pool", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson" + ] + }, + { + "pr": "11742", + "title": "[FIX] Broken slack compatible webhook", + "userLogin": "geekgonecrazy", + "milestone": "0.70.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "11965", + "title": "[NEW] Customizable default directory view", + "userLogin": "ohmonster", + "milestone": "0.70.0", + "contributors": [ + "ohmonster", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12047", + "title": "[NEW] Blockstack as decentralized auth provider", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "timkinnane" + ] + }, + { + "pr": "11900", + "title": "[NEW] Livechat REST endpoints", + "userLogin": "renatobecker", + "milestone": "0.70.0", + "contributors": [ + "renatobecker", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12043", + "title": "[FIX] Changing Mentions.userMentionRegex pattern to include
tag", + "userLogin": "rssilva", + "milestone": "0.70.0", + "contributors": [ + "rssilva", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "11902", + "title": "[FIX] Double output of message actions", + "userLogin": "timkinnane", + "milestone": "0.70.0", + "contributors": [ + "timkinnane", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "11785", + "title": "[FIX] Login error message not obvious if user not activated", + "userLogin": "crazy-max", + "milestone": "0.70.0", + "contributors": [ + "crazy-max", + "web-flow" + ] + }, + { + "pr": "11909", + "title": "[NEW] REST endpoints to get moderators from groups and channels", + "userLogin": "MarcosSpessatto", + "milestone": "0.70.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "11169", + "title": "[NEW] User preference for 24- or 12-hour clock", + "userLogin": "vynmera", + "milestone": "0.70.0", + "contributors": [ + "vynmera", + "ggazzo" + ] + }, + { + "pr": "11919", + "title": "[FIX] Adding scroll bar to read receipts modal", + "userLogin": "rssilva", + "milestone": "0.70.0", + "contributors": [ + "rssilva", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "11946", + "title": "[FIX] Fixing translation on 'yesterday' word when calling timeAgo function", + "userLogin": "rssilva", + "milestone": "0.70.0", + "contributors": [ + "rssilva" + ] + }, + { + "pr": "12018", + "title": "[FIX] Fixing spacement between tags and words on some labels", + "userLogin": "rssilva", + "milestone": "0.70.0", + "contributors": [ + "rssilva" + ] + }, + { + "pr": "12031", + "title": "[FIX] video message recording, issue #11651", + "userLogin": "flaviogrossi", + "milestone": "0.70.0", + "contributors": [ + "flaviogrossi" + ] + }, + { + "pr": "11999", + "title": "[FIX] Prevent form submission in Files List search", + "userLogin": "tassoevan", + "milestone": "0.70.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "11905", + "title": "[NEW] REST endpoint to set groups' announcement", + "userLogin": "MarcosSpessatto", + "milestone": "0.70.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12079", + "title": "[FIX] Re-add the eye-off icon", + "userLogin": "MIKI785", + "contributors": [ + "MIKI785" + ] + }, + { + "pr": "12068", + "title": "[NEW] Livechat trigger option to run only once", + "userLogin": "edzluhan", + "contributors": [ + "edzluhan" + ] + }, + { + "pr": "12044", + "title": "[IMPROVE] Use eslint-config package", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "11953", + "title": "[FIX] Internal error when cross-origin with CORS is disabled", + "userLogin": "MarcosSpessatto", + "milestone": "0.70.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "11967", + "title": "[FIX] Message reaction in GraphQL API", + "userLogin": "MarcosSpessatto", + "milestone": "0.70.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "11863", + "title": "[FIX] Direct messages leaking into logs", + "userLogin": "Hudell", + "milestone": "0.70.0", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "11991", + "title": "Fix the style lint", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11855", + "title": "[NEW] REST endpoints to create roles and assign roles to users", + "userLogin": "aferreira44", + "milestone": "0.70.0", + "contributors": [ + "aferreira44", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12025", + "title": "[FIX] Hidden admin sidenav on embedded layout", + "userLogin": "ggazzo", + "milestone": "0.69.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "11955", + "title": "[FIX] Update user information not possible by admin if disabled to users", + "userLogin": "kaiiiiiiiii", + "milestone": "0.69.2", + "contributors": [ + "kaiiiiiiiii" + ] + }, + { + "pr": "11992", + "title": "[FIX] Apps: setting with 'code' type only saving last line", + "userLogin": "cardoso", + "milestone": "0.69.2", + "contributors": [ + "cardoso" + ] + }, + { + "pr": "12022", + "title": "[FIX] Reset password link error if already logged in", + "userLogin": "rodrigok", + "milestone": "0.69.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11879", + "title": "[FIX] Wrong build path in install.sh", + "userLogin": "geekgonecrazy", + "milestone": "0.69.0", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "11857", + "title": "[FIX] Permission check on joinRoom for private room", + "userLogin": "timkinnane", + "contributors": [ + "timkinnane", + "web-flow" + ] + }, + { + "pr": "11812", + "title": "[NEW] Include room name in stream for bots", + "userLogin": "timkinnane", + "milestone": "0.69.2", + "contributors": [ + "timkinnane", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "9984", + "title": "[NEW] Informal German translations", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "11562", + "title": "[FIX] Close popover on shortcuts and writing", + "userLogin": "MartinSchoeler", + "contributors": [ + "MartinSchoeler" + ] + }, + { + "pr": "11351", + "title": "[BREAK] Update the default port of the Prometheus exporter", + "userLogin": "thaiphv", + "milestone": "0.70.0", + "contributors": [ + "thaiphv", + "web-flow" + ] + }, + { + "pr": "11872", + "title": "[FIX] Typo in a configuration key for SlackBridge excluded bot names", + "userLogin": "TobiasKappe", + "milestone": "0.70.0", + "contributors": [ + "TobiasKappe" + ] + }, + { + "pr": "11921", + "title": "Merge master into develop & Set version to 0.70.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "engelgabriel", + "sampaiodiego", + "tassoevan", + "rodrigok", + "web-flow", + "c0dzilla", + "Hudell", + "rndmh3ro", + "MarcosSpessatto", + "vynmera", + "renatobecker", + "ubarsaiyan" + ] + }, + { + "pr": "11853", + "title": "[FIX] Duplicated message buttons", + "userLogin": "ubarsaiyan", + "milestone": "0.69.1", + "contributors": [ + "ubarsaiyan", + "web-flow" + ] + }, + { + "pr": "11893", + "title": " [FIX] App updates were not being shown correctly", + "userLogin": "rodrigok", + "milestone": "0.69.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11910", + "title": "[FIX] Hipchat importer was not importing users without emails and uploaded files", + "userLogin": "rodrigok", + "milestone": "0.69.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11892", + "title": "[FIX] Hipchat import was failing when importing messages from a non existent user", + "userLogin": "rodrigok", + "milestone": "0.69.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12026", + "title": "Release 0.69.2", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego", + "timkinnane", + "rodrigok", + "cardoso", + "kaiiiiiiiii" + ] + }, + { + "pr": "11812", + "title": "[NEW] Include room name in stream for bots", + "userLogin": "timkinnane", + "milestone": "0.69.2", + "contributors": [ + "timkinnane", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12022", + "title": "[FIX] Reset password link error if already logged in", + "userLogin": "rodrigok", + "milestone": "0.69.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11992", + "title": "[FIX] Apps: setting with 'code' type only saving last line", + "userLogin": "cardoso", + "milestone": "0.69.2", + "contributors": [ + "cardoso" + ] + }, + { + "pr": "11955", + "title": "[FIX] Update user information not possible by admin if disabled to users", + "userLogin": "kaiiiiiiiii", + "milestone": "0.69.2", + "contributors": [ + "kaiiiiiiiii" + ] + }, + { + "pr": "12025", + "title": "[FIX] Hidden admin sidenav on embedded layout", + "userLogin": "ggazzo", + "milestone": "0.69.2", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "0.70.0-rc.1": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12138", + "title": "Regression: fix message box autogrow", + "userLogin": "ggazzo", + "milestone": "0.70.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "12122", + "title": "Regression: Modal height", + "userLogin": "ggazzo", + "milestone": "0.70.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12124", + "title": "Fix: Change wording on e2e to make a little more clear", + "userLogin": "geekgonecrazy", + "milestone": "0.70.0", + "contributors": [ + "geekgonecrazy" + ] + } + ] + }, + "0.70.0-rc.2": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12154", + "title": "[FIX] Real Name on Direct Messages ", + "userLogin": "ggazzo", + "milestone": "0.70.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "12038", + "title": "[FIX] Position of popover component on mobile", + "userLogin": "ggazzo", + "milestone": "0.70.0", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12168", + "title": "[FIX] Duplicate email and auto-join on mentions", + "userLogin": "sampaiodiego", + "milestone": "0.70.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12172", + "title": "Improve: Moved the e2e password request to an alert instead of a popup", + "userLogin": "Hudell", + "milestone": "0.70.0", + "contributors": [ + "Hudell", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12169", + "title": "New: Option to change E2E key", + "userLogin": "Hudell", + "milestone": "0.70.0", + "contributors": [ + "Hudell", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12173", + "title": "Improve: Decrypt last message", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12102", + "title": "[FIX] Horizontal scroll on user info tab", + "userLogin": "rssilva", + "milestone": "0.70.0", + "contributors": [ + "rssilva" + ] + }, + { + "pr": "12140", + "title": "[FIX] Markdown ampersand escape on links", + "userLogin": "rssilva", + "milestone": "0.70.0", + "contributors": [ + "rssilva" + ] + }, + { + "pr": "11938", + "title": "[NEW] Apps: API provider", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12170", + "title": "[FIX] Saving user preferences", + "userLogin": "sampaiodiego", + "milestone": "0.70.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12139", + "title": "Fix: e2e password visible on always-on alert message.", + "userLogin": "Hudell", + "milestone": "0.70.0", + "contributors": [ + "Hudell", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12159", + "title": "[FIX] Apps being able to see hidden settings", + "userLogin": "graywolf336", + "contributors": [ + "graywolf336", + "web-flow" + ] + }, + { + "pr": "12112", + "title": "[FIX] Allow user with \"bulk-register-user\" permission to send invitations", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson", + "web-flow", + "geekgonecrazy" + ] + } + ] + }, + "0.70.0-rc.3": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12196", + "title": "Improve: Expose apps enable setting at `General > Apps`", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "12188", + "title": "Fix: Message changing order when been edited with apps enabled", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12009", + "title": "[BREAK][IMPROVE] New emails design", + "userLogin": "ggazzo", + "milestone": "0.70.0", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "11906", + "title": "[FIX] IRC Federation no longer working", + "userLogin": "Hudell", + "milestone": "0.70.0", + "contributors": [ + "Hudell", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12191", + "title": "Improve: E2E setting description and alert", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12189", + "title": "[NEW] Apps are enabled by default now", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12184", + "title": "[NEW] Add Livechat Analytics permission", + "userLogin": "renatobecker", + "milestone": "0.70.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12192", + "title": "Improve: Do not start E2E Encryption when accessing admin as embedded", + "userLogin": "rodrigok", + "milestone": "0.70.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11679", + "title": "[NEW] WebDAV Integration (User file provider)", + "userLogin": "karakayasemi", + "milestone": "0.70.0", + "contributors": [ + "karakayasemi", + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12187", + "title": "Fix: Add e2e doc to the alert", + "userLogin": "geekgonecrazy", + "milestone": "0.70.0", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "11565", + "title": "[FIX] Files list missing from popover menu when owner of room", + "userLogin": "tassoevan", + "milestone": "Short-term", + "contributors": [ + "tassoevan", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "11927", + "title": "[FIX] Not able to set per-channel retention policies if no global policy is set for this channel type", + "userLogin": "vynmera", + "contributors": [ + "vynmera", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "12126", + "title": "[FIX] app engine verbose log typo", + "userLogin": "williamriancho", + "contributors": [ + "williamriancho", + "web-flow", + "geekgonecrazy" + ] + } + ] + }, + "0.70.0-rc.4": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12195", + "title": "Improve: Switch e2e doc to target _blank", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12175", + "title": "Improve: Rename E2E methods", + "userLogin": "Hudell", + "contributors": [ + "Hudell", + "rodrigok", + "web-flow", + "geekgonecrazy" + ] + } + ] + }, + "0.70.0": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12026", + "title": "Release 0.69.2", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego", + "timkinnane", + "rodrigok", + "cardoso", + "kaiiiiiiiii" + ] + }, + { + "pr": "11812", + "title": "[NEW] Include room name in stream for bots", + "userLogin": "timkinnane", + "milestone": "0.69.2", + "contributors": [ + "timkinnane", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12022", + "title": "[FIX] Reset password link error if already logged in", + "userLogin": "rodrigok", + "milestone": "0.69.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11992", + "title": "[FIX] Apps: setting with 'code' type only saving last line", + "userLogin": "cardoso", + "milestone": "0.69.2", + "contributors": [ + "cardoso" + ] + }, + { + "pr": "11955", + "title": "[FIX] Update user information not possible by admin if disabled to users", + "userLogin": "kaiiiiiiiii", + "milestone": "0.69.2", + "contributors": [ + "kaiiiiiiiii" + ] + }, + { + "pr": "12025", + "title": "[FIX] Hidden admin sidenav on embedded layout", + "userLogin": "ggazzo", + "milestone": "0.69.2", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "0.70.1-rc.0": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12264", + "title": "Merge master into develop & Set version to 0.71.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego", + "timkinnane", + "rodrigok", + "cardoso", + "kaiiiiiiiii", + "web-flow" + ] + }, + { + "pr": "12254", + "title": "[FIX] E2E data not cleared on logout", + "userLogin": "Hudell", + "milestone": "0.70.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12232", + "title": "[FIX] E2E password request not closing after entering password", + "userLogin": "Hudell", + "milestone": "0.70.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12263", + "title": "[FIX] Message editing was duplicating reply quotes", + "userLogin": "rodrigok", + "milestone": "0.70.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12257", + "title": "[FIX] Livechat integration with RDStation", + "userLogin": "sampaiodiego", + "milestone": "0.70.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12233", + "title": "Regression: fix modal submit", + "userLogin": "ggazzo", + "milestone": "0.70.1", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "12255", + "title": "[FIX] Livechat triggers being registered twice after setting department via API", + "userLogin": "edzluhan", + "milestone": "0.70.1", + "contributors": [ + "edzluhan" + ] + }, + { + "pr": "12242", + "title": "[FIX] Livechat CRM integration running when disabled ", + "userLogin": "renatobecker", + "milestone": "0.70.1", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12241", + "title": "[FIX] Emails' logo and links", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "12240", + "title": "[FIX] Set default action for Setup Wizard form submit", + "userLogin": "tassoevan", + "milestone": "0.71.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12227", + "title": "Add reetp to the issues' bot whitelist", + "userLogin": "theorenck", + "contributors": [ + "theorenck", + "web-flow" + ] + } + ] + }, + "0.70.1-rc.1": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12268", + "title": "Fix: Remove semver satisfies from Apps details that is already done my marketplace", + "userLogin": "sampaiodiego", + "milestone": "0.70.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.70.1": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12270", + "title": "Release 0.70.1", + "userLogin": "sampaiodiego", + "contributors": [ + "theorenck", + "rodrigok", + "tassoevan", + "ggazzo", + "sampaiodiego", + "renatobecker", + "edzluhan", + "web-flow", + "Hudell" + ] + } + ] + }, + "0.70.2": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [] + }, + "0.70.3": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12281", + "title": "Release 0.70.3", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12276", + "title": "Release 0.70.2", + "userLogin": "sampaiodiego", + "contributors": [ + "Hudell", + "sampaiodiego" + ] + }, + { + "pr": "12272", + "title": "[FIX] E2E alert shows up when encryption is disabled", + "userLogin": "Hudell", + "milestone": "0.70.2", + "contributors": [ + "Hudell" + ] + } + ] + }, + "0.70.4": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12299", + "title": "Release 0.70.4", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12283", + "title": "[FIX] Modal confirm on enter", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12298", + "title": "Fix: Add wizard opt-in fields", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.71.0-rc.0": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "11521", + "title": "[FIX] Add image dimensions to attachment even when no reorientation is required", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12158", + "title": "[FIX] iframe login token not checked", + "userLogin": "nimetu", + "contributors": [ + "nimetu", + "web-flow" + ] + }, + { + "pr": "11431", + "title": "[FIX] REST `users.setAvatar` endpoint wasn't allowing update the avatar of other users even with correct permissions", + "userLogin": "MarcosSpessatto", + "milestone": "0.71.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego" + ] + }, + { + "pr": "11875", + "title": "[FIX] Slack importer: image previews not showing", + "userLogin": "madguy02", + "milestone": "0.71.0", + "contributors": [ + "madguy02", + "Hudell", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12235", + "title": "[FIX] Edit room name with uppercase letters", + "userLogin": "nikeee", + "milestone": "0.71.0", + "contributors": [ + "nikeee", + "web-flow" + ] + }, + { + "pr": "12256", + "title": "[FIX] Custom OAuth Configuration can't be removed", + "userLogin": "Hudell", + "milestone": "0.71.0", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12360", + "title": "[IMPROVE] Livechat room closure endpoints", + "userLogin": "renatobecker", + "milestone": "0.71.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12266", + "title": "[BREAK] Update `lastMessage` rooms property and convert the \"starred\" property, to the same format", + "userLogin": "MarcosSpessatto", + "milestone": "0.71.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12384", + "title": "Fix: wrong saveUser permission validations", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "11860", + "title": "[NEW] Add delete channel mutation to GraphQL API", + "userLogin": "MarcosSpessatto", + "milestone": "0.71.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12344", + "title": "[FIX] Remove e2e from users endpoint responses", + "userLogin": "MarcosSpessatto", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12372", + "title": "[NEW] sidenav size on large screens", + "userLogin": "ggazzo", + "milestone": "0.71.0", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12375", + "title": "Regression: do not render pdf preview on safari <= 12", + "userLogin": "ggazzo", + "milestone": "0.71.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "12373", + "title": "[FIX] email api TAPi18n is undefined", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "12338", + "title": "[FIX] Blockstack errors in IE 11", + "userLogin": "tassoevan", + "milestone": "0.71.0", + "contributors": [ + "tassoevan", + "sampaiodiego" + ] + }, + { + "pr": "12365", + "title": "[FIX] avatar?_dc=undefined", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12186", + "title": "[BREAK] Add expiration to API login tokens and fix duplicate login tokens created by LDAP", + "userLogin": "MarcosSpessatto", + "milestone": "0.71.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12358", + "title": "Improve: Drop database between running tests on CI", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12161", + "title": "[IMPROVE] Set Livechat department before register guest", + "userLogin": "renatobecker", + "milestone": "0.71.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12330", + "title": "[IMPROVE] Add missing livechat i18n keys", + "userLogin": "MarcosEllys", + "contributors": [ + "MarcosEllys", + "web-flow" + ] + }, + { + "pr": "12297", + "title": "[FIX] users.register endpoint to not create an user if username already being used", + "userLogin": "MarcosSpessatto", + "milestone": "0.71.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12345", + "title": "[FIX] Date range check on livechat analytics", + "userLogin": "teresy", + "contributors": [ + "teresy" + ] + }, + { + "pr": "12194", + "title": "[FIX] Cast env var setting to int based on option type", + "userLogin": "crazy-max", + "milestone": "0.71.0", + "contributors": [ + "crazy-max" + ] + }, + { + "pr": "12355", + "title": "[FIX] Links in home layout", + "userLogin": "upiksaleh", + "contributors": [ + "upiksaleh" + ] + }, + { + "pr": "11212", + "title": "[IMPROVE] Avoid unnecessary calls to Meteor.user() on client", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "12346", + "title": "Fix: update check on err.details", + "userLogin": "teresy", + "milestone": "0.71.0", + "contributors": [ + "teresy" + ] + }, + { + "pr": "12350", + "title": "[FIX] Last message not updating after message delete if show deleted status is on", + "userLogin": "sampaiodiego", + "milestone": "0.71.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12353", + "title": "[NEW] Ability to disable user presence monitor", + "userLogin": "sampaiodiego", + "milestone": "0.71.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12354", + "title": "[FIX] Invalid destructuring on Livechat API endpoint", + "userLogin": "renatobecker", + "milestone": "0.71.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "10519", + "title": "[NEW] PDF message attachment preview (client side rendering)", + "userLogin": "kb0304", + "milestone": "0.71.0", + "contributors": [ + "kb0304", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "12298", + "title": "Fix: Add wizard opt-in fields", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12283", + "title": "[FIX] Modal confirm on enter", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12272", + "title": "[FIX] E2E alert shows up when encryption is disabled", + "userLogin": "Hudell", + "milestone": "0.70.2", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12299", + "title": "Release 0.70.4", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12276", + "title": "Release 0.70.2", + "userLogin": "sampaiodiego", + "contributors": [ + "Hudell", + "sampaiodiego" + ] + } + ] + }, + "0.71.0-rc.1": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12398", + "title": "[FIX] E2E: Decrypting UTF-8 encoded messages", + "userLogin": "pmmaga", + "milestone": "0.71.0", + "contributors": [ + "pmmaga", + "web-flow" + ] + }, + { + "pr": "12442", + "title": "Update Apps Framework to version 1.2.1", + "userLogin": "rodrigok", + "milestone": "0.71.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12424", + "title": "[FIX] Ignore errors when creating image preview for uploads", + "userLogin": "tassoevan", + "milestone": "0.71.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12405", + "title": "Regression: Change `starred` message property from object to array", + "userLogin": "MarcosSpessatto", + "milestone": "0.71.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego" + ] + }, + { + "pr": "12425", + "title": "[IMPROVE] Allow the imports to accept any file type", + "userLogin": "graywolf336", + "milestone": "0.71.0", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "12409", + "title": "Apps: Room’s usernames was not working", + "userLogin": "rodrigok", + "milestone": "0.71.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12436", + "title": "[FIX] Attachment actions not being collapsable", + "userLogin": "graywolf336", + "milestone": "0.71.0", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "12432", + "title": "[NEW] Add \"help wanted\" section to Readme", + "userLogin": "isabellarussell", + "contributors": [ + "isabellarussell", + "Sing-Li", + "web-flow" + ] + }, + { + "pr": "12392", + "title": "Regression: Fix email headers not being used", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.71.0-rc.2": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12445", + "title": "[FIX] Attachment timestamp from and to Apps system not working", + "userLogin": "graywolf336", + "milestone": "0.71.0", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "12391", + "title": "[FIX] Apps not being able to state how the action buttons are aligned", + "userLogin": "graywolf336", + "milestone": "0.71.0", + "contributors": [ + "graywolf336" + ] + } + ] + }, + "0.71.0": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12299", + "title": "Release 0.70.4", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12276", + "title": "Release 0.70.2", + "userLogin": "sampaiodiego", + "contributors": [ + "Hudell", + "sampaiodiego" + ] + } + ] + }, + "0.71.1": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12499", + "title": "Release 0.71.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12487", + "title": "[FIX] Email sending with GDPR user data", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.72.0-rc.0": { + "pull_requests": [ + { + "pr": "12684", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12309", + "title": "[NEW] Add permission to enable personal access token to specific roles", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12563", + "title": "[IMPROVE] Improve unreads and unreadsFrom response, prevent it to be equal null", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12483", + "title": "[NEW] Option to reset e2e key", + "userLogin": "Hudell", + "milestone": "0.72.0", + "contributors": [ + "Hudell", + "web-flow", + "engelgabriel" + ] + }, + { + "pr": "12633", + "title": "[FIX] Fixed Anonymous Registration", + "userLogin": "wreiske", + "milestone": "0.72.0", + "contributors": [ + "wreiske", + "web-flow", + "engelgabriel", + "rodrigok" + ] + }, + { + "pr": "12105", + "title": "[IMPROVE] Add rooms property in user object, if the user has the permission, with rooms roles", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12682", + "title": "Convert rocketchat-mail-messages to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12679", + "title": " Convert rocketchat-livestream to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12675", + "title": "[IMPROVE] border-radius to use --border-radius", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "12677", + "title": "[FIX] high cpu usage ~ svg icon", + "userLogin": "ph1p", + "contributors": [ + null, + "ph1p" + ] + }, + { + "pr": "12374", + "title": "Added \"npm install\" to quick start for developers", + "userLogin": "wreiske", + "milestone": "0.72.0", + "contributors": [ + "wreiske", + "web-flow" + ] + }, + { + "pr": "12651", + "title": "[NEW] /api/v1/spotlight: return joinCodeRequired field for rooms", + "userLogin": "cardoso", + "milestone": "0.72.0", + "contributors": [ + "cardoso", + "web-flow" + ] + }, + { + "pr": "12678", + "title": "Convert rocketchat-ldap to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12674", + "title": "Convert rocketchat-issuelinks to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12670", + "title": "Convert rocketchat-integrations to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12672", + "title": "Convert rocketchat-irc to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12671", + "title": "Convert rocketchat-internal-hubot to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12665", + "title": "Convert rocketchat-importer-hipchat-enterprise to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12669", + "title": "Convert rocketchat-importer-slack-users to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12666", + "title": " Convert rocketchat-importer-slack to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12661", + "title": "Convert rocketchat-iframe-login to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12662", + "title": "Convert rocketchat-importer to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12663", + "title": "Convert rocketchat-importer-csv to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12664", + "title": "Convert rocketchat-importer-hipchat to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12659", + "title": "Convert rocketchat-highlight-words to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12657", + "title": "Convert rocketchat-grant to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12658", + "title": "Convert rocketchat-graphql to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12564", + "title": "[IMPROVE] Update the 'keyboard shortcuts' documentation", + "userLogin": "nicolasbock", + "milestone": "0.72.0", + "contributors": [ + "nicolasbock", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12643", + "title": "[FIX] Fix favico error", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12649", + "title": "Convert rocketchat-google-vision to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12650", + "title": "Removed RocketChatFile from globals", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12623", + "title": "[NEW] New API Endpoints for the new version of JS SDK", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "engelgabriel", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12647", + "title": "Added imports for global variables in rocketchat-google-natural-language package", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12646", + "title": "Convert rocketchat-gitlab to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12644", + "title": "Convert rocketchat-file to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12642", + "title": "Convert rocketchat-github-enterprise to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12645", + "title": "Fix: Add email dependency in package.js", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12561", + "title": "[IMPROVE] Add new acceptable header for Livechat REST requests", + "userLogin": "renatobecker", + "milestone": "0.72.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12599", + "title": "Convert rocketchat-custom-sounds to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12641", + "title": "Fix crowd error with import of SyncedCron", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12605", + "title": "Convert emoji-emojione to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12607", + "title": "Convert rocketchat-favico to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12604", + "title": " Convert rocketchat-emoji-custom to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12632", + "title": "[FIX] Condition to not render PDF preview", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12606", + "title": "Convert rocketchat-error-handler to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12601", + "title": "Convert rocketchat-drupal to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12596", + "title": "Convert rocketchat-crowd to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12603", + "title": "Convert rocketchat-emoji to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12625", + "title": "Fix users.setAvatar endpoint tests and logic", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12616", + "title": "[IMPROVE] Atlassian Crowd settings and option to sync user data", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + null, + "rodrigok", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12583", + "title": "[DOCS] Remove Cordova links, include F-Droid download button and few other adjustments", + "userLogin": "rafaelks", + "contributors": [ + "rafaelks", + "web-flow" + ] + }, + { + "pr": "12600", + "title": "Convert rocketchat-dolphin to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12594", + "title": " Convert rocketchat-channel-settings to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12595", + "title": "Convert rocketchat-cors to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12618", + "title": "[IMPROVE] CircleCI to use MongoDB 4.0 for testing", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "12614", + "title": "[FIX] Admin styles", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel" + ] + }, + { + "pr": "12602", + "title": "[FIX] Admin styles", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel" + ] + }, + { + "pr": "9336", + "title": "[FIX] Change registration message when user need to confirm email", + "userLogin": "karlprieb", + "milestone": "0.72.0", + "contributors": [ + "karlprieb", + "tassoevan", + "ggazzo", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12570", + "title": "[FIX] Import missed file in rocketchat-authorization", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12558", + "title": "[FIX] Prevent subscriptions and calls to rooms events that the user is not participating", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12382", + "title": "[IMPROVE] Japanese translations", + "userLogin": "ura14h", + "contributors": [ + "ura14h", + "web-flow" + ] + }, + { + "pr": "12525", + "title": "[IMPROVE] Add CTRL modifier for keyboard shortcut", + "userLogin": "nicolasbock", + "milestone": "0.72.0", + "contributors": [ + "nicolasbock" + ] + }, + { + "pr": "12530", + "title": "Convert rocketchat-autotranslate to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12537", + "title": "Convert rocketchat-channel-settings-mail-messages to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12538", + "title": "Convert rocketchat-colors to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12532", + "title": "Convert rocketchat-cas to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12531", + "title": "Convert rocketchat-bot-helpers to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12529", + "title": "Convert rocketchat-autolinker to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12523", + "title": "Convert rocketchat-authorization to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12547", + "title": "[NEW] Setting to configure robots.txt content", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12539", + "title": "[FIX] Wrong test case for `users.setAvatar` endpoint", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego" + ] + }, + { + "pr": "12536", + "title": "[FIX] Spotlight method being called multiple times", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12524", + "title": "Fix CSS import order", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12518", + "title": "[FIX] German translation for for API_EmbedIgnoredHosts label", + "userLogin": "mbrodala", + "milestone": "0.72.0", + "contributors": [ + "mbrodala" + ] + }, + { + "pr": "12426", + "title": "Remove template for feature requests as issues", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12451", + "title": "Fix punctuation, spelling, and grammar", + "userLogin": "imronras", + "milestone": "0.72.0", + "contributors": [ + "imronras", + "web-flow" + ] + }, + { + "pr": "12522", + "title": "[IMPROVE] Ignore non-existent Livechat custom fields on Livechat API", + "userLogin": "renatobecker", + "milestone": "0.72.0", + "contributors": [ + "renatobecker", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12507", + "title": "[FIX] Handle all events for enter key in message box", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12378", + "title": "[NEW] Make Livechat's widget draggable", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12408", + "title": "[FIX] Fix wrong parameter in chat.delete endpoint and add some test cases", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12452", + "title": "[IMPROVE] Emoji search on messageBox behaving like emojiPicker's search (#9607)", + "userLogin": "vinade", + "milestone": "0.72.0", + "contributors": [ + "vinade" + ] + }, + { + "pr": "12521", + "title": "Convert rocketchat-assets to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12510", + "title": "Convert rocketchat-api to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12506", + "title": "Convert rocketchat-analytics to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12503", + "title": "Convert rocketchat-action-links to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12501", + "title": "Convert rocketchat-2fa to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12495", + "title": "Convert meteor-timesync to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12491", + "title": "Convert meteor-autocomplete package to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12486", + "title": "Convert meteor-accounts-saml to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12485", + "title": "Convert chatpal search package to modular structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12467", + "title": "Removal of TAPi18n and TAPi18next global variables", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12433", + "title": "Removal of Template, Blaze, BlazeLayout, FlowRouter, DDPRateLimiter, Session, UAParser, Promise, Reload and CryptoJS global variables", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "12410", + "title": "Removal of Match, check, moment, Tracker and Mongo global variables", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "12377", + "title": "Removal of EJSON, Accounts, Email, HTTP, Random, ReactiveDict, ReactiveVar, SHA256 and WebApp global variables", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12371", + "title": "Removal of Meteor global variable", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12468", + "title": "[BREAK] Update to Meteor to 1.8", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12509", + "title": "Fix ES translation", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12487", + "title": "[FIX] Email sending with GDPR user data", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12471", + "title": "[IMPROVE] German translations", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson", + "web-flow" + ] + }, + { + "pr": "12470", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12397", + "title": "[FIX] Manage own integrations permissions check", + "userLogin": "ggazzo", + "milestone": "0.72.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12411", + "title": "[FIX] stream room-changed", + "userLogin": "ggazzo", + "milestone": "0.72.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "12457", + "title": "[FIX] Emoji picker is not in viewport on small screens", + "userLogin": "ramrami", + "milestone": "0.72.0", + "contributors": [ + "ramrami" + ] + }, + { + "pr": "12400", + "title": "[IMPROVE] Limit the number of typing users shown (#8722)", + "userLogin": "vinade", + "contributors": [ + "vinade", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12406", + "title": "[FIX] `Disabled` word translation to Spanish", + "userLogin": "Ismaw34", + "contributors": [ + "Ismaw34", + "web-flow" + ] + }, + { + "pr": "12260", + "title": "[FIX] `Disabled` word translation to Chinese", + "userLogin": "AndreamApp", + "milestone": "0.72.0", + "contributors": [ + "AndreamApp", + "web-flow" + ] + }, + { + "pr": "12465", + "title": "Update npm dependencies", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12440", + "title": "Fix: Developers not being able to debug root files in VSCode", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson" + ] + }, + { + "pr": "12453", + "title": "[FIX] Correct roomName value in Mail Messages (#12363)", + "userLogin": "vinade", + "milestone": "0.72.0", + "contributors": [ + "vinade" + ] + }, + { + "pr": "12460", + "title": "Merge master into develop & Set version to 0.72.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "Hudell" + ] + }, + { + "pr": "12499", + "title": "Release 0.71.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12487", + "title": "[FIX] Email sending with GDPR user data", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.72.0-rc.1": { + "pull_requests": [ + { + "pr": "12712", + "title": "Fix some Ukrainian translations", + "userLogin": "zdumitru", + "milestone": "0.72.0", + "contributors": [ + "zdumitru", + "web-flow" + ] + }, + { + "pr": "12713", + "title": "[FIX] Update caret position on insert a new line in message box", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12714", + "title": "[IMPROVE] Allow apps to update persistence by association", + "userLogin": "marceloschmidt", + "contributors": [ + "marceloschmidt", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12708", + "title": "Improve: Add missing translation keys.", + "userLogin": "ura14h", + "milestone": "0.72.0", + "contributors": [ + "ura14h", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12705", + "title": "Bump Apps Engine to 1.3.0", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12699", + "title": "Fix: Exception when registering a user with gravatar", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "engelgabriel", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12637", + "title": "[FIX] DE translation for idle-time-limit", + "userLogin": "pfuender", + "milestone": "0.72.0", + "contributors": [ + null, + "engelgabriel", + "web-flow", + "pfuender" + ] + }, + { + "pr": "12707", + "title": "Fix: Fix tests by increasing window size", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12680", + "title": "[IMPROVE] Add more methods to deal with rooms via Rocket.Chat.Apps", + "userLogin": "marceloschmidt", + "milestone": "0.72.0", + "contributors": [ + "marceloschmidt" + ] + }, + { + "pr": "12692", + "title": "[IMPROVE] Better query for finding subscriptions that need a new E2E Key", + "userLogin": "Hudell", + "milestone": "0.72.0", + "contributors": [ + "Hudell" + ] + } + ] + }, + "0.72.0-rc.2": { + "pull_requests": [ + { + "pr": "12741", + "title": "Update Apps Engine to 1.3.1", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12736", + "title": "Regression: Expand Administration sections by toggling section title", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12737", + "title": "Regression: Fix Safari detection in PDF previewing", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12735", + "title": "Regression: Account pages layout", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12729", + "title": "Regression: Inherit font-family for message box", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "0.72.0": { + "pull_requests": [ + { + "pr": "12499", + "title": "Release 0.71.1", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12487", + "title": "[FIX] Email sending with GDPR user data", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.70.5": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12898", + "title": "[FIX] Reset password email", + "userLogin": "sampaiodiego", + "milestone": "0.72.2", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.71.2": { + "node_version": "8.11.3", + "npm_version": "5.6.0", + "pull_requests": [ + { + "pr": "12898", + "title": "[FIX] Reset password email", + "userLogin": "sampaiodiego", + "milestone": "0.72.2", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.73.0-rc.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "pull_requests": [ + { + "pr": "12985", + "title": "[IMPROVE] Hipchat Enterprise Importer", + "userLogin": "Hudell", + "milestone": "0.73.0", + "contributors": [ + "Hudell", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13014", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "milestone": "0.73.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12123", + "title": "[FIX] Avoiding links with highlighted words", + "userLogin": "rssilva", + "milestone": "0.73.0", + "contributors": [ + "rssilva", + "tassoevan", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12748", + "title": "[NEW] Create new permission.listAll endpoint to be able to use updatedSince parameter", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "9748", + "title": "[NEW] Mandatory 2fa for role", + "userLogin": "Hudell", + "milestone": "0.73.0", + "contributors": [ + "Hudell", + "karlprieb", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12739", + "title": "[FIX] Pin and unpin message were not checking permissions", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12734", + "title": "[FIX] Fix users.setPreferences endpoint, set language correctly", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12754", + "title": "[NEW] Add query parameter support to emoji-custom endpoint", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12790", + "title": "[FIX] Fix set avatar http call, to avoid SSL errors", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12856", + "title": "[NEW] Added a link to contributing.md", + "userLogin": "sanketsingh24", + "milestone": "0.73.0", + "contributors": [ + "sanketsingh24", + "web-flow" + ] + }, + { + "pr": "13010", + "title": "[NEW] Added chat.getDeletedMessages since specific date", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12569", + "title": "[FIX] Webdav integration account settings were being shown even when Webdav was disabled", + "userLogin": "karakayasemi", + "milestone": "0.73.0", + "contributors": [ + "karakayasemi" + ] + }, + { + "pr": "12722", + "title": "[IMPROVE] Add missing translation keys.", + "userLogin": "ura14h", + "milestone": "0.73.0", + "contributors": [ + "ura14h", + "web-flow" + ] + }, + { + "pr": "12792", + "title": "[FIX] Provide better Dutch translations 🇳🇱", + "userLogin": "mathysie", + "milestone": "0.73.0", + "contributors": [ + "mathysie", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12795", + "title": "[FIX] E2E`s password reaveal text is always `>%S` when language is zh", + "userLogin": "lvyue", + "milestone": "0.73.0", + "contributors": [ + "lvyue", + "web-flow" + ] + }, + { + "pr": "12874", + "title": "[NEW] Download button for each file in fileslist", + "userLogin": "alexbartsch", + "milestone": "0.73.0", + "contributors": [ + "alexbartsch", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "13011", + "title": "Move isFirefox and isChrome functions to rocketchat-utils", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12998", + "title": "[FIX] Nested Markdown blocks not parsed properly", + "userLogin": "Hudell", + "milestone": "0.73.0", + "contributors": [ + "Hudell", + "rodrigok" + ] + }, + { + "pr": "13009", + "title": "[IMPROVE] Accept Slash Commands via Action Buttons when `msg_in_chat_window: true`", + "userLogin": "rodrigok", + "milestone": "0.73.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13008", + "title": "[IMPROVE] Allow transfer Livechats to online agents only", + "userLogin": "renatobecker", + "milestone": "0.73.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12706", + "title": "[FIX] Change JSON to EJSON.parse query to support type Date", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13005", + "title": "Move tapi18n t and isRtl functions from ui to utils", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13004", + "title": "[FIX] Inherit font family in message user card", + "userLogin": "tassoevan", + "milestone": "0.73.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12999", + "title": "Remove /* globals */ wave 4", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12997", + "title": "Remove /* globals */ wave 3", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12995", + "title": "Convert rocketchat-logger to main module structure and remove Logger from eslintrc", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12948", + "title": "[FIX] Some deprecation issues for media recording", + "userLogin": "tassoevan", + "milestone": "0.72.4", + "contributors": [ + "tassoevan", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12983", + "title": "[FIX] Stop click event propagation on mention link or user card", + "userLogin": "tassoevan", + "milestone": "0.73.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12973", + "title": "[FIX] Change field checks in RocketChat.saveStreamingOptions", + "userLogin": "tassoevan", + "milestone": "0.73.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12980", + "title": "[FIX] Remove sharp's deprecation warnings on image upload", + "userLogin": "tassoevan", + "milestone": "0.73.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12975", + "title": "[FIX] Use web.browser.legacy bundle for Livechat script", + "userLogin": "tassoevan", + "milestone": "0.72.4", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12954", + "title": "[FIX] Revert Jitsi external API to an asset", + "userLogin": "sampaiodiego", + "milestone": "0.72.4", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12970", + "title": "[FIX] Exception in getSingleMessage", + "userLogin": "tsukiRep", + "milestone": "0.72.4", + "contributors": [ + "tsukiRep", + "web-flow" + ] + }, + { + "pr": "12988", + "title": "Remove /* globals */ wave 2", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12984", + "title": "Remove /* globals */ from files wave-1", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12940", + "title": "[FIX] multiple rooms-changed", + "userLogin": "ggazzo", + "milestone": "0.72.4", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "12994", + "title": "[FIX] Readable validation on the apps engine environment bridge", + "userLogin": "d-gubert", + "milestone": "0.73.0", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "12989", + "title": "[IMPROVE] Adding debugging instructions in README", + "userLogin": "hypery2k", + "contributors": [ + "hypery2k", + "web-flow" + ] + }, + { + "pr": "12972", + "title": "[FIX] Check for object falsehood before referencing properties in saveRoomSettings", + "userLogin": "tassoevan", + "milestone": "0.72.4", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12959", + "title": "Move globals of test to a specific eslintrc file", + "userLogin": "rodrigok", + "milestone": "0.73.0", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12960", + "title": "Remove global ServiceConfiguration", + "userLogin": "rodrigok", + "milestone": "0.73.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12961", + "title": "Remove global toastr", + "userLogin": "rodrigok", + "milestone": "0.73.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12942", + "title": "Convert rocketchat-livechat to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12957", + "title": "[FIX] Spotlight being called while in background", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12949", + "title": "changed maxRoomsOpen", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "12934", + "title": "Revert imports of css, reAdd them to the addFiles function", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12896", + "title": "Convert rocketchat-theme to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12895", + "title": "Convert rocketchat-katex to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12886", + "title": "Convert rocketchat-webdav to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12871", + "title": "Convert rocketchat-ui-message to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12881", + "title": "Convert rocketchat-videobridge to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12556", + "title": "[FIX] Padding for message box in embedded layout", + "userLogin": "tassoevan", + "milestone": "0.72.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12930", + "title": "[FIX] Crowd sync was being stopped when a user was not found", + "userLogin": "piotrkochan", + "milestone": "0.72.3", + "contributors": [ + "piotrkochan", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12913", + "title": "[FIX] Some icons were missing", + "userLogin": "tassoevan", + "milestone": "0.72.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12829", + "title": "[FIX] User data download fails when a room has been deleted.", + "userLogin": "Hudell", + "milestone": "0.72.3", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12860", + "title": "[FIX] CAS Login not working with renamed users", + "userLogin": "Hudell", + "milestone": "0.72.3", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12914", + "title": "[FIX] Stream of my_message wasn't sending the room information", + "userLogin": "ggazzo", + "milestone": "0.72.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "12903", + "title": "[FIX] cannot reset password", + "userLogin": "ggazzo", + "milestone": "0.72.3", + "contributors": [ + "ggazzo", + "web-flow", + "Hudell" + ] + }, + { + "pr": "12904", + "title": "[IMPROVE] Do not emit settings if there are no changes", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12905", + "title": "[FIX] Version check update notification", + "userLogin": "rodrigok", + "milestone": "0.72.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12900", + "title": "[FIX] line-height for unread bar buttons (jump to first and mark as read)", + "userLogin": "tassoevan", + "milestone": "0.72.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12882", + "title": "[FIX] PDF view loading indicator", + "userLogin": "tassoevan", + "milestone": "0.72.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12898", + "title": "[FIX] Reset password email", + "userLogin": "sampaiodiego", + "milestone": "0.72.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12888", + "title": " Convert rocketchat-reactions to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12887", + "title": "Convert rocketchat-wordpress to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12883", + "title": "Fix: snap push from ci", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12865", + "title": "[IMPROVE] Returning an open room object in the Livechat config endpoint", + "userLogin": "renatobecker", + "milestone": "0.73.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12867", + "title": " [NEW] Syncloud deploy option", + "userLogin": "cyberb", + "contributors": [ + "cyberb", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "12879", + "title": "Convert rocketchat-version-check to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12351", + "title": "[NEW] Config hooks for snap", + "userLogin": "LuluGO", + "contributors": [ + "LuluGO", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12597", + "title": "[NEW] Livechat registration form message", + "userLogin": "renatobecker", + "milestone": "0.73.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "12877", + "title": "Convert rocketchat-user-data-dowload to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12875", + "title": "Convert rocketchat-ui-vrecord to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12861", + "title": "Convert rocketchat-ui-login to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12859", + "title": " Convert rocketchat-ui-flextab to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12866", + "title": "[FIX] Data Import not working", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12771", + "title": "[NEW] Include message type & id in push notification payload", + "userLogin": "cardoso", + "contributors": [ + "cardoso", + "web-flow" + ] + }, + { + "pr": "12851", + "title": "[FIX] Incorrect parameter name in Livechat stream", + "userLogin": "renatobecker", + "milestone": "0.73.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12585", + "title": "[FIX] Autotranslate icon on message action menu", + "userLogin": "marceloschmidt", + "milestone": "0.73.0", + "contributors": [ + "marceloschmidt", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "12761", + "title": "German translation typo fix for Reacted_with", + "userLogin": "localguru", + "contributors": [ + "localguru" + ] + }, + { + "pr": "12848", + "title": "Bump Apps-Engine version", + "userLogin": "d-gubert", + "milestone": "0.72.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "12818", + "title": "[FIX] Change spread operator to Array.from for Edge browser", + "userLogin": "ohmonster", + "milestone": "0.72.1", + "contributors": [ + "ohmonster" + ] + }, + { + "pr": "12842", + "title": " Convert rocketchat-ui-account to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12727", + "title": "[FIX] API users.info returns caller rooms and not requested user ones", + "userLogin": "piotrkochan", + "milestone": "0.72.1", + "contributors": [ + "piotrkochan", + "web-flow" + ] + }, + { + "pr": "12804", + "title": "Change file order in rocketchat-cors", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.1", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12847", + "title": "[FIX] Missing HipChat Enterprise Importer", + "userLogin": "Hudell", + "milestone": "0.72.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12846", + "title": "Convert rocketchat-ui-clean-history to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12844", + "title": "Convert rocketchat-ui-admin to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12843", + "title": "[FIX] Google Cloud Storage storage provider", + "userLogin": "sampaiodiego", + "milestone": "0.73.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12838", + "title": "Convert rocketchat-tokenpass to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12840", + "title": "Remove rocketchat-tutum package", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12839", + "title": "Convert rocketchat-tooltip to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12837", + "title": "Convert rocketchat-token-login to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12566", + "title": "[IMPROVE] Use MongoBD aggregation to get users from a room", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "engelgabriel", + "sampaiodiego" + ] + }, + { + "pr": "12833", + "title": "Convert rocketchat-statistics to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12832", + "title": "Convert rocketchat-spotify to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12831", + "title": "Convert rocketchat-sms to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12801", + "title": "Convert rocketchat-search to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12767", + "title": "Convert rocketchat-message-pin to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12770", + "title": "Convert rocketchat-message-star to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12823", + "title": "Convert rocketchat-slashcommands-msg to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12830", + "title": "Convert rocketchat-smarsh-connector to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12828", + "title": "Convert rocketchat-slider to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12827", + "title": "Convert rocketchat-slashcommands-unarchiveroom to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12624", + "title": "Dependencies update", + "userLogin": "engelgabriel", + "milestone": "0.73.0", + "contributors": [ + "engelgabriel", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12826", + "title": "Convert rocketchat-slashcommands-topic to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12825", + "title": "Convert rocketchat-slashcommands-open to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12824", + "title": "Convert rocketchat-slashcommands-mute to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12822", + "title": "Convert rocketchat-slashcommands-me to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12821", + "title": "Convert rocketchat-slashcommands-leave to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12817", + "title": "Convert rocketchat-slashcommands-kick to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12816", + "title": "Convert rocketchat-slashcommands-join to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12815", + "title": "Convert rocketchat-slashcommands-inviteall to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12814", + "title": "Convert rocketchat-slashcommands-invite to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12813", + "title": "Convert rocketchat-slashcommands-hide to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12812", + "title": "Convert rocketchat-slashcommands-help to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12811", + "title": "Convert rocketchat-slashcommands-create to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12810", + "title": "Convert rocketchat-slashcomands-archiveroom to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12808", + "title": "Convert rocketchat-slashcommands-asciiarts to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12807", + "title": "Convert rocketchat-slackbridge to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12806", + "title": "Convert rocketchat-setup-wizard to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12799", + "title": "Convert rocketchat-sandstorm to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12773", + "title": "Convert rocketchat-oauth2-server-config to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12768", + "title": "Convert rocketchat-message-snippet to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12805", + "title": "[FIX] Emoji as avatar", + "userLogin": "tassoevan", + "milestone": "0.72.1", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12779", + "title": "[IMPROVE] Username suggestion logic", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "12803", + "title": "Fix CI deploy job", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12797", + "title": "Convert rocketchat-retention-policy to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12778", + "title": "Convert rocketchat-push-notifications to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12777", + "title": "Convert rocketchat-otr to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12775", + "title": "Convert rocketchat-oembed to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12772", + "title": "Convert rocketchat-migrations to main-module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12766", + "title": "Convert rocketchat-message-mark-as-unread to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12798", + "title": "Remove conventional changelog cli, we are using our own cli now (Houston)", + "userLogin": "rodrigok", + "milestone": "0.73.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12760", + "title": "Convert rocketchat-message-attachments to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12759", + "title": "Convert rocketchat-message-action to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12757", + "title": " Convert rocketchat-mentions-flextab to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12756", + "title": "Convert rocketchat-mentions to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12755", + "title": "Convert rocketchat-markdown to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12701", + "title": "Convert rocketchat-mapview to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "12791", + "title": "Add check to make sure releases was updated", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12776", + "title": "Merge master into develop & Set version to 0.73.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12741", + "title": "Update Apps Engine to 1.3.1", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12736", + "title": "Regression: Expand Administration sections by toggling section title", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12737", + "title": "Regression: Fix Safari detection in PDF previewing", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12735", + "title": "Regression: Account pages layout", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12729", + "title": "Regression: Inherit font-family for message box", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12712", + "title": "Fix some Ukrainian translations", + "userLogin": "zdumitru", + "milestone": "0.72.0", + "contributors": [ + "zdumitru", + "web-flow" + ] + }, + { + "pr": "12713", + "title": "[FIX] Update caret position on insert a new line in message box", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12714", + "title": "[IMPROVE] Allow apps to update persistence by association", + "userLogin": "marceloschmidt", + "contributors": [ + "marceloschmidt", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12708", + "title": "Improve: Add missing translation keys.", + "userLogin": "ura14h", + "milestone": "0.72.0", + "contributors": [ + "ura14h", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12705", + "title": "Bump Apps Engine to 1.3.0", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12699", + "title": "Fix: Exception when registering a user with gravatar", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "engelgabriel", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12637", + "title": "[FIX] DE translation for idle-time-limit", + "userLogin": "pfuender", + "milestone": "0.72.0", + "contributors": [ + null, + "engelgabriel", + "web-flow", + "pfuender" + ] + }, + { + "pr": "12707", + "title": "Fix: Fix tests by increasing window size", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12680", + "title": "[IMPROVE] Add more methods to deal with rooms via Rocket.Chat.Apps", + "userLogin": "marceloschmidt", + "milestone": "0.72.0", + "contributors": [ + "marceloschmidt" + ] + }, + { + "pr": "12692", + "title": "[IMPROVE] Better query for finding subscriptions that need a new E2E Key", + "userLogin": "Hudell", + "milestone": "0.72.0", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12684", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12309", + "title": "[NEW] Add permission to enable personal access token to specific roles", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12563", + "title": "[IMPROVE] Improve unreads and unreadsFrom response, prevent it to be equal null", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12483", + "title": "[NEW] Option to reset e2e key", + "userLogin": "Hudell", + "milestone": "0.72.0", + "contributors": [ + "Hudell", + "web-flow", + "engelgabriel" + ] + }, + { + "pr": "12633", + "title": "[FIX] Fixed Anonymous Registration", + "userLogin": "wreiske", + "milestone": "0.72.0", + "contributors": [ + "wreiske", + "web-flow", + "engelgabriel", + "rodrigok" + ] + }, + { + "pr": "12105", + "title": "[IMPROVE] Add rooms property in user object, if the user has the permission, with rooms roles", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12682", + "title": "Convert rocketchat-mail-messages to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12679", + "title": " Convert rocketchat-livestream to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12675", + "title": "[IMPROVE] border-radius to use --border-radius", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "12677", + "title": "[FIX] high cpu usage ~ svg icon", + "userLogin": "ph1p", + "contributors": [ + null, + "ph1p" + ] + }, + { + "pr": "12374", + "title": "Added \"npm install\" to quick start for developers", + "userLogin": "wreiske", + "milestone": "0.72.0", + "contributors": [ + "wreiske", + "web-flow" + ] + }, + { + "pr": "12651", + "title": "[NEW] /api/v1/spotlight: return joinCodeRequired field for rooms", + "userLogin": "cardoso", + "milestone": "0.72.0", + "contributors": [ + "cardoso", + "web-flow" + ] + }, + { + "pr": "12678", + "title": "Convert rocketchat-ldap to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12674", + "title": "Convert rocketchat-issuelinks to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12670", + "title": "Convert rocketchat-integrations to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12672", + "title": "Convert rocketchat-irc to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12671", + "title": "Convert rocketchat-internal-hubot to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12665", + "title": "Convert rocketchat-importer-hipchat-enterprise to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12669", + "title": "Convert rocketchat-importer-slack-users to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12666", + "title": " Convert rocketchat-importer-slack to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12661", + "title": "Convert rocketchat-iframe-login to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12662", + "title": "Convert rocketchat-importer to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12663", + "title": "Convert rocketchat-importer-csv to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12664", + "title": "Convert rocketchat-importer-hipchat to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12659", + "title": "Convert rocketchat-highlight-words to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12657", + "title": "Convert rocketchat-grant to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12658", + "title": "Convert rocketchat-graphql to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12564", + "title": "[IMPROVE] Update the 'keyboard shortcuts' documentation", + "userLogin": "nicolasbock", + "milestone": "0.72.0", + "contributors": [ + "nicolasbock", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12643", + "title": "[FIX] Fix favico error", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12649", + "title": "Convert rocketchat-google-vision to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12650", + "title": "Removed RocketChatFile from globals", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12623", + "title": "[NEW] New API Endpoints for the new version of JS SDK", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "engelgabriel", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12647", + "title": "Added imports for global variables in rocketchat-google-natural-language package", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12646", + "title": "Convert rocketchat-gitlab to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12644", + "title": "Convert rocketchat-file to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12642", + "title": "Convert rocketchat-github-enterprise to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12645", + "title": "Fix: Add email dependency in package.js", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12561", + "title": "[IMPROVE] Add new acceptable header for Livechat REST requests", + "userLogin": "renatobecker", + "milestone": "0.72.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "12599", + "title": "Convert rocketchat-custom-sounds to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12641", + "title": "Fix crowd error with import of SyncedCron", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12605", + "title": "Convert emoji-emojione to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12607", + "title": "Convert rocketchat-favico to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12604", + "title": " Convert rocketchat-emoji-custom to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12632", + "title": "[FIX] Condition to not render PDF preview", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12606", + "title": "Convert rocketchat-error-handler to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12601", + "title": "Convert rocketchat-drupal to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12596", + "title": "Convert rocketchat-crowd to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12603", + "title": "Convert rocketchat-emoji to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12625", + "title": "Fix users.setAvatar endpoint tests and logic", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12616", + "title": "[IMPROVE] Atlassian Crowd settings and option to sync user data", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + null, + "rodrigok", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12583", + "title": "[DOCS] Remove Cordova links, include F-Droid download button and few other adjustments", + "userLogin": "rafaelks", + "contributors": [ + "rafaelks", + "web-flow" + ] + }, + { + "pr": "12600", + "title": "Convert rocketchat-dolphin to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12594", + "title": " Convert rocketchat-channel-settings to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12595", + "title": "Convert rocketchat-cors to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12618", + "title": "[IMPROVE] CircleCI to use MongoDB 4.0 for testing", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "12614", + "title": "[FIX] Admin styles", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel" + ] + }, + { + "pr": "12602", + "title": "[FIX] Admin styles", + "userLogin": "engelgabriel", + "milestone": "0.72.0", + "contributors": [ + "engelgabriel" + ] + }, + { + "pr": "9336", + "title": "[FIX] Change registration message when user need to confirm email", + "userLogin": "karlprieb", + "milestone": "0.72.0", + "contributors": [ + "karlprieb", + "tassoevan", + "ggazzo", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12570", + "title": "[FIX] Import missed file in rocketchat-authorization", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12558", + "title": "[FIX] Prevent subscriptions and calls to rooms events that the user is not participating", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12382", + "title": "[IMPROVE] Japanese translations", + "userLogin": "ura14h", + "contributors": [ + "ura14h", + "web-flow" + ] + }, + { + "pr": "12525", + "title": "[IMPROVE] Add CTRL modifier for keyboard shortcut", + "userLogin": "nicolasbock", + "milestone": "0.72.0", + "contributors": [ + "nicolasbock" + ] + }, + { + "pr": "12530", + "title": "Convert rocketchat-autotranslate to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12537", + "title": "Convert rocketchat-channel-settings-mail-messages to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12538", + "title": "Convert rocketchat-colors to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12532", + "title": "Convert rocketchat-cas to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12531", + "title": "Convert rocketchat-bot-helpers to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12529", + "title": "Convert rocketchat-autolinker to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12523", + "title": "Convert rocketchat-authorization to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12547", + "title": "[NEW] Setting to configure robots.txt content", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12539", + "title": "[FIX] Wrong test case for `users.setAvatar` endpoint", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego" + ] + }, + { + "pr": "12536", + "title": "[FIX] Spotlight method being called multiple times", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12524", + "title": "Fix CSS import order", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12518", + "title": "[FIX] German translation for for API_EmbedIgnoredHosts label", + "userLogin": "mbrodala", + "milestone": "0.72.0", + "contributors": [ + "mbrodala" + ] + }, + { + "pr": "12426", + "title": "Remove template for feature requests as issues", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12451", + "title": "Fix punctuation, spelling, and grammar", + "userLogin": "imronras", + "milestone": "0.72.0", + "contributors": [ + "imronras", + "web-flow" + ] + }, + { + "pr": "12522", + "title": "[IMPROVE] Ignore non-existent Livechat custom fields on Livechat API", + "userLogin": "renatobecker", + "milestone": "0.72.0", + "contributors": [ + "renatobecker", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12507", + "title": "[FIX] Handle all events for enter key in message box", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12378", + "title": "[NEW] Make Livechat's widget draggable", + "userLogin": "tassoevan", + "milestone": "0.72.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12408", + "title": "[FIX] Fix wrong parameter in chat.delete endpoint and add some test cases", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12452", + "title": "[IMPROVE] Emoji search on messageBox behaving like emojiPicker's search (#9607)", + "userLogin": "vinade", + "milestone": "0.72.0", + "contributors": [ + "vinade" + ] + }, + { + "pr": "12521", + "title": "Convert rocketchat-assets to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12510", + "title": "Convert rocketchat-api to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12506", + "title": "Convert rocketchat-analytics to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12503", + "title": "Convert rocketchat-action-links to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12501", + "title": "Convert rocketchat-2fa to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12495", + "title": "Convert meteor-timesync to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12491", + "title": "Convert meteor-autocomplete package to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12486", + "title": "Convert meteor-accounts-saml to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12485", + "title": "Convert chatpal search package to modular structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12467", + "title": "Removal of TAPi18n and TAPi18next global variables", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto" + ] + }, + { + "pr": "12433", + "title": "Removal of Template, Blaze, BlazeLayout, FlowRouter, DDPRateLimiter, Session, UAParser, Promise, Reload and CryptoJS global variables", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "12410", + "title": "Removal of Match, check, moment, Tracker and Mongo global variables", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "12377", + "title": "Removal of EJSON, Accounts, Email, HTTP, Random, ReactiveDict, ReactiveVar, SHA256 and WebApp global variables", + "userLogin": "rodrigok", + "milestone": "0.72.0", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12371", + "title": "Removal of Meteor global variable", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "12468", + "title": "[BREAK] Update to Meteor to 1.8", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "12509", + "title": "Fix ES translation", + "userLogin": "tassoevan", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12487", + "title": "[FIX] Email sending with GDPR user data", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12471", + "title": "[IMPROVE] German translations", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson", + "web-flow" + ] + }, + { + "pr": "12470", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12397", + "title": "[FIX] Manage own integrations permissions check", + "userLogin": "ggazzo", + "milestone": "0.72.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12411", + "title": "[FIX] stream room-changed", + "userLogin": "ggazzo", + "milestone": "0.72.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "12457", + "title": "[FIX] Emoji picker is not in viewport on small screens", + "userLogin": "ramrami", + "milestone": "0.72.0", + "contributors": [ + "ramrami" + ] + }, + { + "pr": "12400", + "title": "[IMPROVE] Limit the number of typing users shown (#8722)", + "userLogin": "vinade", + "contributors": [ + "vinade", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "12406", + "title": "[FIX] `Disabled` word translation to Spanish", + "userLogin": "Ismaw34", + "contributors": [ + "Ismaw34", + "web-flow" + ] + }, + { + "pr": "12260", + "title": "[FIX] `Disabled` word translation to Chinese", + "userLogin": "AndreamApp", + "milestone": "0.72.0", + "contributors": [ + "AndreamApp", + "web-flow" + ] + }, + { + "pr": "12465", + "title": "Update npm dependencies", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12440", + "title": "Fix: Developers not being able to debug root files in VSCode", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson" + ] + }, + { + "pr": "12453", + "title": "[FIX] Correct roomName value in Mail Messages (#12363)", + "userLogin": "vinade", + "milestone": "0.72.0", + "contributors": [ + "vinade" + ] + }, + { + "pr": "12460", + "title": "Merge master into develop & Set version to 0.72.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "web-flow", + "Hudell" + ] + } + ] + }, + "0.72.1": { + "pull_requests": [ + { + "pr": "12850", + "title": "Release 0.72.1", + "userLogin": "rodrigok", + "contributors": [ + "tassoevan", + "rodrigok", + "Hudell", + "MarcosSpessatto", + "piotrkochan", + "ohmonster", + "d-gubert", + "sampaiodiego" + ] + }, + { + "pr": "12848", + "title": "Bump Apps-Engine version", + "userLogin": "d-gubert", + "milestone": "0.72.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "12818", + "title": "[FIX] Change spread operator to Array.from for Edge browser", + "userLogin": "ohmonster", + "milestone": "0.72.1", + "contributors": [ + "ohmonster" + ] + }, + { + "pr": "12727", + "title": "[FIX] API users.info returns caller rooms and not requested user ones", + "userLogin": "piotrkochan", + "milestone": "0.72.1", + "contributors": [ + "piotrkochan", + "web-flow" + ] + }, + { + "pr": "12804", + "title": "Change file order in rocketchat-cors", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.1", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12847", + "title": "[FIX] Missing HipChat Enterprise Importer", + "userLogin": "Hudell", + "milestone": "0.72.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12805", + "title": "[FIX] Emoji as avatar", + "userLogin": "tassoevan", + "milestone": "0.72.1", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "0.72.2": { + "pull_requests": [ + { + "pr": "12901", + "title": "Release 0.72.2", + "userLogin": "sampaiodiego", + "contributors": [ + "tassoevan", + "sampaiodiego" + ] + }, + { + "pr": "12900", + "title": "[FIX] line-height for unread bar buttons (jump to first and mark as read)", + "userLogin": "tassoevan", + "milestone": "0.72.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "12898", + "title": "[FIX] Reset password email", + "userLogin": "sampaiodiego", + "milestone": "0.72.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12882", + "title": "[FIX] PDF view loading indicator", + "userLogin": "tassoevan", + "milestone": "0.72.2", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "0.72.3": { + "pull_requests": [ + { + "pr": "12932", + "title": "Release 0.72.3", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "ggazzo", + "Hudell", + "tassoevan", + "piotrkochan" + ] + } + ] + }, + "0.73.0-rc.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "pull_requests": [ + { + "pr": "13021", + "title": "Change `chat.getDeletedMessages` to get messages after informed date and return only message's _id", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13020", + "title": "Improve Importer code quality", + "userLogin": "Hudell", + "milestone": "0.73.0", + "contributors": [ + "Hudell" + ] + } + ] + }, + "0.73.0-rc.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "pull_requests": [ + { + "pr": "13031", + "title": "Regression: List of custom emojis wasn't working", + "userLogin": "MarcosSpessatto", + "milestone": "0.73.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13033", + "title": "[FIX] Download files without extension wasn't possible", + "userLogin": "tassoevan", + "milestone": "0.73.0", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "0.73.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "pull_requests": [ + { + "pr": "12932", + "title": "Release 0.72.3", + "userLogin": "rodrigok", + "contributors": [ + "rodrigok", + "ggazzo", + "Hudell", + "tassoevan", + "piotrkochan" + ] + }, + { + "pr": "12901", + "title": "Release 0.72.2", + "userLogin": "sampaiodiego", + "contributors": [ + "tassoevan", + "sampaiodiego" + ] + }, + { + "pr": "12848", + "title": "Bump Apps-Engine version", + "userLogin": "d-gubert", + "milestone": "0.72.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "12818", + "title": "[FIX] Change spread operator to Array.from for Edge browser", + "userLogin": "ohmonster", + "milestone": "0.72.1", + "contributors": [ + "ohmonster" + ] + }, + { + "pr": "12727", + "title": "[FIX] API users.info returns caller rooms and not requested user ones", + "userLogin": "piotrkochan", + "milestone": "0.72.1", + "contributors": [ + "piotrkochan", + "web-flow" + ] + }, + { + "pr": "12804", + "title": "Change file order in rocketchat-cors", + "userLogin": "MarcosSpessatto", + "milestone": "0.72.1", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12847", + "title": "[FIX] Missing HipChat Enterprise Importer", + "userLogin": "Hudell", + "milestone": "0.72.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12805", + "title": "[FIX] Emoji as avatar", + "userLogin": "tassoevan", + "milestone": "0.72.1", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "0.73.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13052", + "title": "Release 0.73.1", + "userLogin": "rodrigok", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego", + "rodrigok" + ] + }, + { + "pr": "13049", + "title": "Execute tests with versions 3.2, 3.4, 3.6 and 4.0 of MongoDB", + "userLogin": "rodrigok", + "milestone": "0.73.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13051", + "title": "Regression: Get room's members list not working on MongoDB 3.2", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13045", + "title": "[FIX] Default importer path", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.73.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13086", + "title": "Release 0.73.2", + "userLogin": "sampaiodiego", + "contributors": [ + "graywolf336", + "sampaiodiego" + ] + }, + { + "pr": "13013", + "title": "[NEW] Cloud Integration", + "userLogin": "graywolf336", + "milestone": "0.73.2", + "contributors": [ + "graywolf336", + "geekgonecrazy", + "web-flow", + "sampaiodiego" + ] + } + ] + }, + "0.74.0-rc.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "12294", + "title": "[IMPROVE] Dutch translations", + "userLogin": "Jeroeny", + "contributors": [ + "Jeroeny", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13112", + "title": "[FIX] Few polish translating", + "userLogin": "theundefined", + "contributors": [ + "theundefined" + ] + }, + { + "pr": "13114", + "title": "[IMPROVE] Persian translations", + "userLogin": "behnejad", + "contributors": [ + "behnejad", + "web-flow" + ] + }, + { + "pr": "13201", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13053", + "title": "[FIX] Update Message: Does not show edited when message was not edited.", + "userLogin": "Kailash0311", + "contributors": [ + "Kailash0311", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13067", + "title": "[FIX] Notifications for mentions not working on large rooms and don't emit desktop notifications for offline users", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "12153", + "title": "[NEW] SAML: Adds possibility to decrypt encrypted assertions", + "userLogin": "gerbsen", + "milestone": "0.74.0", + "contributors": [ + "gerbsen", + "web-flow" + ] + }, + { + "pr": "13177", + "title": "Language: Edit typo \"Обновлить\"", + "userLogin": "zpavlig", + "milestone": "0.74.0", + "contributors": [ + "zpavlig", + "web-flow" + ] + }, + { + "pr": "11251", + "title": "[NEW] Add rate limiter to REST endpoints", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "geekgonecrazy", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "12858", + "title": "[FIX] Emoticons not displayed in room topic", + "userLogin": "alexbartsch", + "milestone": "0.74.0", + "contributors": [ + "alexbartsch", + "web-flow" + ] + }, + { + "pr": "13129", + "title": "[IMPROVE] Change the way the app detail screen shows support link when it's an email", + "userLogin": "d-gubert", + "milestone": "0.74.0", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13183", + "title": "[NEW] Added an option to disable email when activate and deactivate users", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13160", + "title": "[NEW] Add create, update and delete endpoint for custom emojis", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13150", + "title": "[FIX] REST API endpoint `users.getPersonalAccessTokens` error when user has no access tokens", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13192", + "title": "Regression: Fix export AudioRecorder", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13167", + "title": "[NEW] Added endpoint to update timeout of the jitsi video conference", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13188", + "title": "[FIX] Remove unused code for Cordova", + "userLogin": "rodrigok", + "milestone": "0.74.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13194", + "title": "[IMPROVE] Process alerts from update checking", + "userLogin": "rodrigok", + "milestone": "0.74.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13184", + "title": "[NEW] Display total number of files and total upload size in admin", + "userLogin": "geekgonecrazy", + "milestone": "0.74.0", + "contributors": [ + "geekgonecrazy", + "sampaiodiego", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "13181", + "title": "[FIX] Avatars with transparency were being converted to black", + "userLogin": "rodrigok", + "milestone": "0.74.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13180", + "title": "[FIX] REST api client base url on subdir", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "13170", + "title": "[FIX] Change webdav creation, due to changes in the npm lib after last update", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "12927", + "title": "[FIX] Invite command was not accpeting @ in username", + "userLogin": "piotrkochan", + "milestone": "0.74.0", + "contributors": [ + "piotrkochan", + "marceloschmidt", + "web-flow" + ] + }, + { + "pr": "13105", + "title": "[FIX] Remove ES6 code from Livechat widget script", + "userLogin": "tassoevan", + "milestone": "0.74.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13169", + "title": "[IMPROVE] Add \"Apps Engine Version\" to Administration > Info", + "userLogin": "d-gubert", + "milestone": "0.74.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "12982", + "title": "[NEW] Livechat GDPR compliance", + "userLogin": "renatobecker", + "milestone": "0.74.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13168", + "title": "[IMPROVE] New Livechat statistics added to statistics collector", + "userLogin": "renatobecker", + "milestone": "0.74.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13076", + "title": "[NEW] Added stream to notify when agent status change", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13078", + "title": "[IMPROVE] Return room type field on Livechat findRoom method", + "userLogin": "renatobecker", + "milestone": "0.74.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13097", + "title": "[IMPROVE] Return visitorEmails field on Livechat findGuest method", + "userLogin": "renatobecker", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13108", + "title": "[NEW] Add new Livechat REST endpoint to update the visitor's status", + "userLogin": "renatobecker", + "milestone": "0.74.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13158", + "title": "[IMPROVE] Adds the \"showConnecting\" property to Livechat Config payload", + "userLogin": "renatobecker", + "milestone": "0.74.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13137", + "title": " Remove dependency of RocketChat namespace and push-notifications", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13136", + "title": "Remove dependency of RocketChat namespace and custom-sounds", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13135", + "title": "Remove dependency of RocketChat namespace and logger", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13133", + "title": "Remove dependency between RocketChat namespace and migrations", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13132", + "title": "Convert rocketchat:ui to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13131", + "title": "Remove dependency of RocketChat namespace inside rocketchat:ui", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13123", + "title": "Move some ui function to ui-utils", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13157", + "title": "Regression: fix upload permissions", + "userLogin": "sampaiodiego", + "milestone": "0.74.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13122", + "title": "Move some function to utils", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13118", + "title": "Remove directly dependency between rocketchat:lib and emoji", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13117", + "title": "Convert rocketchat-webrtc to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13115", + "title": "Remove directly dependency between lib and e2e", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13107", + "title": "Convert rocketchat-ui-master to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13146", + "title": "Regression: fix rooms model's collection name", + "userLogin": "sampaiodiego", + "milestone": "0.74.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13098", + "title": "Convert rocketchat-ui-sidenav to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13094", + "title": "Convert rocketchat-file-upload to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13066", + "title": "Remove dependency between lib and authz", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13037", + "title": "Globals/main module custom oauth", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13064", + "title": "Move UI Collections to rocketchat:models", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13036", + "title": "Rocketchat mailer", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13039", + "title": "Move rocketchat promises", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13035", + "title": "Globals/move rocketchat notifications", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13119", + "title": "Test only MongoDB with oplog versions 3.2 and 4.0 for PRs", + "userLogin": "rodrigok", + "milestone": "0.74.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13034", + "title": "Move/create rocketchat callbacks", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13032", + "title": "Move/create rocketchat metrics", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13027", + "title": "Move rocketchat models", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13096", + "title": "[FIX] User status on header and user info are not translated", + "userLogin": "sampaiodiego", + "milestone": "0.74.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13091", + "title": "[FIX] #11692 - Suppress error when drop collection in migration to suit to …", + "userLogin": "Xuhao", + "contributors": [ + "Xuhao", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13073", + "title": "[NEW] Add Allow Methods directive to CORS", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13026", + "title": "Move rocketchat settings to specific package", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13013", + "title": "[NEW] Cloud Integration", + "userLogin": "graywolf336", + "milestone": "0.73.2", + "contributors": [ + "graywolf336", + "geekgonecrazy", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13077", + "title": "[FIX] Change input type of e2e to password", + "userLogin": "supra08", + "milestone": "0.74.0", + "contributors": [ + "supra08" + ] + }, + { + "pr": "13074", + "title": "Remove incorrect pt-BR translation", + "userLogin": "tassoevan", + "milestone": "0.74.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13049", + "title": "Execute tests with versions 3.2, 3.4, 3.6 and 4.0 of MongoDB", + "userLogin": "rodrigok", + "milestone": "0.73.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13051", + "title": "Regression: Get room's members list not working on MongoDB 3.2", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13045", + "title": "[FIX] Default importer path", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13050", + "title": "Merge master into develop & Set version to 0.74.0-develop", + "userLogin": "rodrigok", + "contributors": [ + "tassoevan", + "rodrigok", + "Hudell", + "MarcosSpessatto", + "piotrkochan", + "ohmonster", + "d-gubert", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13086", + "title": "Release 0.73.2", + "userLogin": "sampaiodiego", + "contributors": [ + "graywolf336", + "sampaiodiego" + ] + }, + { + "pr": "13049", + "title": "Execute tests with versions 3.2, 3.4, 3.6 and 4.0 of MongoDB", + "userLogin": "rodrigok", + "milestone": "0.73.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13051", + "title": "Regression: Get room's members list not working on MongoDB 3.2", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13045", + "title": "[FIX] Default importer path", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.74.0-rc.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13224", + "title": "Regression: Fix audio message upload", + "userLogin": "sampaiodiego", + "milestone": "0.74.0-rc.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13213", + "title": "Regression: Fix message pinning", + "userLogin": "TkTech", + "milestone": "0.74.0-rc.1", + "contributors": [ + "TkTech" + ] + }, + { + "pr": "13203", + "title": "[FIX] LDAP login of new users overwriting `fname` from all subscriptions", + "userLogin": "sampaiodiego", + "milestone": "0.74.0-rc.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13153", + "title": "[FIX] Snap upgrade add post-refresh hook", + "userLogin": "LuluGO", + "milestone": "0.74.0-rc.1", + "contributors": [ + "LuluGO", + "geekgonecrazy", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "13083", + "title": "[IMPROVE] Adds history log for all Importers and improves HipChat import performance", + "userLogin": "Hudell", + "milestone": "0.74.0-rc.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "13207", + "title": "Regression: Fix emoji search", + "userLogin": "sampaiodiego", + "milestone": "0.74.0-rc.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.74.0-rc.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13266", + "title": "[IMPROVE] Inject metrics on callbacks", + "userLogin": "sampaiodiego", + "milestone": "0.74.0-rc.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13239", + "title": "Change apps engine persistence bridge method to updateByAssociations", + "userLogin": "d-gubert", + "milestone": "0.74.0-rc.2", + "contributors": [ + "d-gubert" + ] + } + ] + }, + "0.74.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13270", + "title": "Release 0.74.0", + "userLogin": "sampaiodiego", + "contributors": [ + "rodrigok", + "web-flow", + "sampaiodiego", + "tassoevan", + "supra08", + "graywolf336", + "MarcosSpessatto", + "Xuhao", + "d-gubert" + ] + }, + { + "pr": "13213", + "title": "Regression: Fix message pinning", + "userLogin": "TkTech", + "milestone": "0.74.0-rc.1", + "contributors": [ + "TkTech" + ] + }, + { + "pr": "13112", + "title": "[FIX] Few polish translating", + "userLogin": "theundefined", + "contributors": [ + "theundefined" + ] + }, + { + "pr": "13086", + "title": "Release 0.73.2", + "userLogin": "sampaiodiego", + "contributors": [ + "graywolf336", + "sampaiodiego" + ] + }, + { + "pr": "13049", + "title": "Execute tests with versions 3.2, 3.4, 3.6 and 4.0 of MongoDB", + "userLogin": "rodrigok", + "milestone": "0.73.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13051", + "title": "Regression: Get room's members list not working on MongoDB 3.2", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13045", + "title": "[FIX] Default importer path", + "userLogin": "sampaiodiego", + "milestone": "0.73.1", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "0.74.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13311", + "title": "[NEW] Limit all DDP/Websocket requests (configurable via admin panel)", + "userLogin": "rodrigok", + "milestone": "0.74.1", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13322", + "title": "[FIX] Mobile view and re-enable E2E tests", + "userLogin": "sampaiodiego", + "milestone": "0.74.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13308", + "title": "[NEW] REST endpoint to forward livechat rooms", + "userLogin": "renatobecker", + "milestone": "0.74.1", + "contributors": [ + "renatobecker", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13293", + "title": "[FIX] Hipchat Enterprise Importer not generating subscriptions", + "userLogin": "Hudell", + "milestone": "0.74.1", + "contributors": [ + "Hudell", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13294", + "title": "[FIX] Message updating by Apps", + "userLogin": "sampaiodiego", + "milestone": "0.74.1", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13306", + "title": "[FIX] REST endpoint for creating custom emojis", + "userLogin": "sampaiodiego", + "milestone": "0.74.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13303", + "title": "[FIX] Preview of image uploads were not working when apps framework is enable", + "userLogin": "rodrigok", + "milestone": "0.74.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13221", + "title": "[FIX] HipChat Enterprise importer fails when importing a large amount of messages (millions)", + "userLogin": "Hudell", + "milestone": "0.74.1", + "contributors": [ + "Hudell", + "tassoevan" + ] + }, + { + "pr": "11525", + "title": "[NEW] Collect data for Monthly/Daily Active Users for a future dashboard", + "userLogin": "renatobecker", + "milestone": "0.74.1", + "contributors": [ + "renatobecker", + "rodrigok" + ] + }, + { + "pr": "13248", + "title": "[NEW] Add parseUrls field to the apps message converter", + "userLogin": "d-gubert", + "milestone": "0.74.1", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13282", + "title": "Fix: Missing export in cloud package", + "userLogin": "geekgonecrazy", + "milestone": "0.74.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12341", + "title": "[FIX] Fix bug when user try recreate channel or group with same name and remove room from cache when user leaves room", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.1", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + } + ] + }, + "0.74.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13326", + "title": "[FIX] Rate Limiter was limiting communication between instances", + "userLogin": "rodrigok", + "milestone": "0.74.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13349", + "title": "[FIX] Setup wizard calling 'saveSetting' for each field/setting", + "userLogin": "ggazzo", + "milestone": "0.74.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "13342", + "title": "[IMPROVE] Send `uniqueID` to all clients so Jitsi rooms can be created correctly", + "userLogin": "sampaiodiego", + "milestone": "0.74.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13350", + "title": "[FIX] Pass token for cloud register", + "userLogin": "geekgonecrazy", + "milestone": "0.74.2", + "contributors": [ + "geekgonecrazy" + ] + } + ] + }, + "0.74.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13474", + "title": "Release 0.74.3", + "userLogin": "sampaiodiego", + "contributors": [ + "tassoevan", + "sampaiodiego", + "graywolf336", + "Hudell", + "d-gubert", + "rodrigok", + "BehindLoader", + "leonboot", + "renatobecker" + ] + }, + { + "pr": "13471", + "title": "Room loading improvements", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13360", + "title": "[FIX] Invalid condition on getting next livechat agent over REST API endpoint", + "userLogin": "renatobecker", + "milestone": "0.74.3", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13417", + "title": "[IMPROVE] Open rooms quicker", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13457", + "title": "[FIX] \"Test Desktop Notifications\" not triggering a notification", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13463", + "title": "[FIX] Translated and incorrect i18n variables", + "userLogin": "leonboot", + "milestone": "0.74.3", + "contributors": [ + "leonboot", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13456", + "title": "Regression: Remove console.log on email translations", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13408", + "title": "[FIX] Properly escape custom emoji names for pattern matching", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13452", + "title": "[FIX] Not translated emails", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13437", + "title": "[FIX] XML-decryption module not found", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "13244", + "title": "[FIX] Update Russian localization", + "userLogin": "BehindLoader", + "milestone": "0.74.3", + "contributors": [ + "BehindLoader", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "13436", + "title": "[IMPROVE] Allow configure Prometheus port per process via Environment Variable", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13430", + "title": "[IMPROVE] Add API option \"permissionsRequired\"", + "userLogin": "d-gubert", + "milestone": "0.74.3", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13336", + "title": "[FIX] Several Problems on HipChat Importer", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "Hudell", + "web-flow" + ] + }, + { + "pr": "13423", + "title": "[FIX] Invalid push gateway configuration, requires the uniqueId", + "userLogin": "graywolf336", + "milestone": "0.74.3", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "13369", + "title": "[FIX] Notify private settings changes even on public settings changed", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13407", + "title": "[FIX] Misaligned upload progress bar \"cancel\" button", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.0.0-rc.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13757", + "title": "[IMPROVE] UI of page not found", + "userLogin": "fliptrail", + "milestone": "1.0.0", + "contributors": [ + "fliptrail", + "engelgabriel", + "web-flow", + "sampaiodiego", + "geekgonecrazy" + ] + }, + { + "pr": "13951", + "title": "[FIX] Opening a Livechat room from another agent", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13938", + "title": "[FIX] Directory and Apps logs page", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13521", + "title": "[FIX] Minor issues detected after testing the new Livechat client", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13896", + "title": "[FIX] Display first message when taking Livechat inquiry", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13953", + "title": "[FIX] Loading theme CSS on first server startup", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13755", + "title": "[FIX] OTR dialog issue", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13966", + "title": "Update eslint config", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13949", + "title": "[FIX] Limit App’s HTTP calls to 500ms", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "sampaiodiego", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "13954", + "title": "Remove some bad references to messageBox", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13964", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13948", + "title": "[IMPROVE] Show rooms with mentions on unread category even with hide counter", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13947", + "title": "Update preview Dockerfile to use Stretch dependencies", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13946", + "title": "Small improvements to federation callbacks/hooks", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13936", + "title": "Improve: Support search and adding federated users through regular endpoints", + "userLogin": "alansikora", + "contributors": [ + "alansikora", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13935", + "title": "Remove bitcoin link in Readme.md since the link is broken", + "userLogin": "ashwaniYDV", + "contributors": [ + "ashwaniYDV", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "13832", + "title": "[FIX] Read Receipt for Livechat Messages fixed", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13809", + "title": "[NEW] Marketplace integration with Rocket.Chat Cloud", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "graywolf336", + "rodrigok", + "geekgonecrazy", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "12626", + "title": "[NEW] Add message action to copy message to input as reply", + "userLogin": "mrsimpson", + "milestone": "1.0.0", + "contributors": [ + "mrsimpson", + "rodrigok", + "web-flow", + "d-gubert" + ] + }, + { + "pr": "13914", + "title": "[FIX] Avatar image being shrinked on autocomplete", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13910", + "title": "Fix missing dependencies on stretch CI image", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13855", + "title": "[FIX] VIDEO/JITSI multiple calls before video call", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "engelgabriel", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13772", + "title": "Remove some index.js files routing for server/client files", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13906", + "title": "Use CircleCI Debian Stretch images", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13895", + "title": "[FIX] Some Safari bugs", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13851", + "title": "[FIX] wrong width/height for tile_70 (mstile 70x70 (png))", + "userLogin": "ulf-f", + "contributors": [ + "ulf-f", + "web-flow" + ] + }, + { + "pr": "13891", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13874", + "title": "User remove role dialog fixed", + "userLogin": "bhardwajaditya", + "contributors": [ + "bhardwajaditya" + ] + }, + { + "pr": "13863", + "title": "[FIX] wrong importing of e2e", + "userLogin": "marceloschmidt", + "milestone": "1.0.0", + "contributors": [ + "marceloschmidt" + ] + }, + { + "pr": "13752", + "title": "[IMPROVE] Join channels by sending a message or join button (#13752)", + "userLogin": "bhardwajaditya", + "milestone": "1.0.0", + "contributors": [ + "bhardwajaditya", + "engelgabriel", + "web-flow", + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "13819", + "title": "[NEW] Allow sending long messages as attachments", + "userLogin": "marceloschmidt", + "milestone": "1.0.0", + "contributors": [ + "marceloschmidt", + "ggazzo" + ] + }, + { + "pr": "13782", + "title": "Rename Threads to Discussion", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13730", + "title": "[IMPROVE] Filter agents with autocomplete input instead of select element", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13818", + "title": "[IMPROVE] Ignore agent status when queuing incoming livechats via Guest Pool", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13806", + "title": "[BUG] Icon Fixed for Knowledge base on Livechat ", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "13803", + "title": "Add support to search for all users in directory", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13839", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13834", + "title": "Remove unused style", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13833", + "title": "Remove unused files", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13825", + "title": "Lingohub sync and additional fixes", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13783", + "title": "[FIX] Forwarded Livechat visitor name is not getting updated on the sidebar", + "userLogin": "zolbayars", + "milestone": "1.0.0", + "contributors": [ + "zolbayars", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13801", + "title": "[FIX] Remove spaces in some i18n files", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13789", + "title": "Fix: addRoomAccessValidator method created for Threads", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13796", + "title": "[IMPROVE] Replaces color #13679A to #1d74f5", + "userLogin": "fliptrail", + "contributors": [ + "fliptrail" + ] + }, + { + "pr": "13751", + "title": "[FIX] Translation interpolations for many languages", + "userLogin": "fliptrail", + "milestone": "1.0.0", + "contributors": [ + "fliptrail", + "engelgabriel", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13779", + "title": "Adds French translation of Personal Access Token", + "userLogin": "ashwaniYDV", + "milestone": "1.0.0", + "contributors": [ + "ashwaniYDV" + ] + }, + { + "pr": "13775", + "title": "[NEW] Add e-mail field on Livechat Departments", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13559", + "title": "[FIX] Fixed grammatical error.", + "userLogin": "gsunit", + "milestone": "1.0.0", + "contributors": [ + "gsunit", + "web-flow" + ] + }, + { + "pr": "13784", + "title": "[FIX] In home screen Rocket.Chat+ is dispalyed as Rocket.Chat", + "userLogin": "ashwaniYDV", + "contributors": [ + "ashwaniYDV" + ] + }, + { + "pr": "13753", + "title": "[FIX] No new room created when conversation is closed", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13773", + "title": "Remove Sandstorm support", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13769", + "title": "[FIX] Loading user list from room messages", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13767", + "title": "Removing (almost) every dynamic imports", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13744", + "title": "[FIX] User is unable to enter multiple emojis by clicking on the emoji icon", + "userLogin": "Kailash0311", + "milestone": "1.0.0", + "contributors": [ + "Kailash0311", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13743", + "title": "[IMPROVE] Remove unnecessary \"File Upload\".", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "13741", + "title": "Regression: Threads styles improvement", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13723", + "title": "[NEW] Provide new Livechat client as community feature", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13727", + "title": "[FIX] Audio message recording", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13740", + "title": "Convert imports to relative paths", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13729", + "title": "Regression: removed backup files", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "13725", + "title": "Remove unused files", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13724", + "title": "[BREAK] Remove deprecated file upload engine Slingshot", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "12429", + "title": "[FIX] Remove Room info for Direct Messages (#9383)", + "userLogin": "vinade", + "milestone": "1.0.0", + "contributors": [ + "vinade", + "ggazzo" + ] + }, + { + "pr": "13726", + "title": "[IMPROVE] Add index for room's ts", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13675", + "title": "[FIX] WebRTC wasn't working duo to design and browser's APIs changes", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego", + "tassoevan" + ] + }, + { + "pr": "13714", + "title": "[FIX] Adds Proper Language display name for many languages", + "userLogin": "fliptrail", + "milestone": "1.0.0", + "contributors": [ + "fliptrail" + ] + }, + { + "pr": "13707", + "title": "Add Houston config", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13705", + "title": "[FIX] Update bad-words to 3.0.2", + "userLogin": "trivoallan", + "contributors": [ + "trivoallan" + ] + }, + { + "pr": "13672", + "title": "[FIX] Changing Room name updates the webhook", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "13702", + "title": "[FIX] Fix snap refresh hook", + "userLogin": "LuluGO", + "contributors": [ + "LuluGO" + ] + }, + { + "pr": "13695", + "title": "Change the way to resolve DNS for Federation", + "userLogin": "alansikora", + "contributors": [ + "alansikora" + ] + }, + { + "pr": "13687", + "title": "Update husky config", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13683", + "title": "Regression: Prune Threads", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13486", + "title": "[FIX] Audio message recording issues", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13677", + "title": "[FIX] Legal pages' style", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13676", + "title": "[FIX] Stop livestream", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "13679", + "title": "Regression: Fix icon for DMs", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13681", + "title": "[FIX] Avatar fonts for PNG and JPG", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12347", + "title": "[IMPROVE] Add decoding for commonName (cn) and displayName attributes for SAML", + "userLogin": "pkolmann", + "milestone": "1.0.0", + "contributors": [ + null, + "pkolmann", + "web-flow", + "engelgabriel", + "sampaiodiego" + ] + }, + { + "pr": "13630", + "title": "[FIX] Block User Icon", + "userLogin": "knrt10", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "13670", + "title": "[FIX] Corrects UI background of forced F2A Authentication", + "userLogin": "fliptrail", + "milestone": "1.0.0", + "contributors": [ + "fliptrail", + "ggazzo" + ] + }, + { + "pr": "13674", + "title": "Regression: Add missing translations used in Apps pages", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "tassoevan" + ] + }, + { + "pr": "13587", + "title": "[FIX] Race condition on the loading of Apps on the admin page", + "userLogin": "graywolf336", + "contributors": [ + "graywolf336", + "sampaiodiego" + ] + }, + { + "pr": "13656", + "title": "Regression: User Discussions join message", + "userLogin": "bhardwajaditya", + "contributors": [ + "bhardwajaditya" + ] + }, + { + "pr": "13658", + "title": "Regression: Sidebar create new channel hover text", + "userLogin": "bhardwajaditya", + "contributors": [ + "bhardwajaditya" + ] + }, + { + "pr": "13574", + "title": "Regression: Fix embedded layout", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13651", + "title": "Improve: Send cloud token to Federation Hub", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13646", + "title": "Regression: Discussions - Invite users and DM", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13541", + "title": "[NEW] Discussions", + "userLogin": "ggazzo", + "contributors": [ + "mrsimpson", + "vickyokrm" + ] + }, + { + "pr": "13635", + "title": "[NEW] Bosnian lang (BS)", + "userLogin": "fliptrail", + "milestone": "1.0.0", + "contributors": [ + "fliptrail" + ] + }, + { + "pr": "13629", + "title": "[FIX] Do not allow change avatars of another users without permission", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13623", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12370", + "title": "[NEW] Federation", + "userLogin": "alansikora", + "milestone": "1.0.0", + "contributors": [ + "alansikora", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13612", + "title": "[FIX] link of k8s deploy", + "userLogin": "Mr-Linus", + "contributors": [ + "Mr-Linus", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "13245", + "title": "[FIX] Bugfix markdown Marked link new tab", + "userLogin": "DeviaVir", + "milestone": "1.0.0", + "contributors": [ + "DeviaVir", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13530", + "title": "[NEW] Show department field on Livechat visitor panel", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13599", + "title": "[FIX] Partially messaging formatting for bold letters", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "13367", + "title": "Force some words to translate in other languages", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "soltanabadiyan", + "tassoevan" + ] + }, + { + "pr": "13442", + "title": "[FIX] Change userId of rate limiter, change to logged user", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13199", + "title": "[FIX] Add retries to docker-compose.yml, to wait for MongoDB to be ready", + "userLogin": "tiangolo", + "milestone": "1.0.0", + "contributors": [ + "tiangolo", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13601", + "title": "Fix wrong imports", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13310", + "title": "[NEW] Add offset parameter to channels.history, groups.history, dm.history", + "userLogin": "xbolshe", + "milestone": "1.0.0", + "contributors": [ + "xbolshe", + "web-flow" + ] + }, + { + "pr": "13467", + "title": "[FIX] Non-latin room names and other slugifications", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13598", + "title": "[IMPROVE] Deprecate fixCordova helper", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13056", + "title": "[FIX] Fixed rocketchat-oembed meta fragment pulling", + "userLogin": "wreiske", + "milestone": "1.0.0", + "contributors": [ + "wreiske", + "web-flow" + ] + }, + { + "pr": "13299", + "title": "Fix: Some german translations", + "userLogin": "soenkef", + "milestone": "1.0.0", + "contributors": [ + "soenkef", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13428", + "title": "[FIX] Attachments without dates were showing December 31, 1970", + "userLogin": "wreiske", + "milestone": "1.0.0", + "contributors": [ + "wreiske", + "web-flow" + ] + }, + { + "pr": "13451", + "title": "[FIX] Restart required to apply changes in API Rate Limiter settings", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13472", + "title": "Add better positioning for tooltips on edges", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13597", + "title": "[NEW] Permission to assign roles", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13563", + "title": "[FIX] Ability to activate an app installed by zip even offline", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "12095", + "title": "[NEW] reply with a file", + "userLogin": "rssilva", + "milestone": "1.0.0", + "contributors": [ + "rssilva", + "geekgonecrazy", + "web-flow", + "ggazzo", + "tassoevan" + ] + }, + { + "pr": "13468", + "title": "[FIX] .bin extension added to attached file names", + "userLogin": "Hudell", + "milestone": "1.0.0", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "12472", + "title": "[NEW] legal notice page", + "userLogin": "localguru", + "milestone": "1.0.0", + "contributors": [ + "localguru", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13502", + "title": "[FIX] Right arrows in default HTML content", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "13469", + "title": "[FIX] Typo in a referrer header in inject.js file", + "userLogin": "algomaster99", + "milestone": "1.0.0", + "contributors": [ + "algomaster99", + "web-flow" + ] + }, + { + "pr": "12952", + "title": "[FIX] Fix issue cannot \u001dfilter channels by name", + "userLogin": "huydang284", + "milestone": "1.0.0", + "contributors": [ + "huydang284", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "11745", + "title": "[FIX] mention-links not being always resolved", + "userLogin": "mrsimpson", + "contributors": [ + "mrsimpson", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "13586", + "title": "Fix: Mongo.setConnectionOptions was not being set correctly", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13439", + "title": "[FIX] allow user to logout before set username", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "13584", + "title": "[IMPROVE] Remove dangling side-nav styles", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13573", + "title": "Regression: Missing settings import at `packages/rocketchat-livechat/server/methods/saveAppearance.js`", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13553", + "title": "[FIX] Error when recording data into the connection object", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13508", + "title": "Depack: Use mainModule for root files", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13564", + "title": "[FIX] Handle showing/hiding input in messageBox", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13567", + "title": "Regression: fix app pages styles", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13388", + "title": "[IMPROVE] Disable X-Powered-By header in all known express middlewares", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "12981", + "title": "[IMPROVE] Allow custom rocketchat username for crowd users and enable login via email/crowd_username", + "userLogin": "steerben", + "milestone": "1.0.0", + "contributors": [ + "steerben", + "web-flow", + "rodrigok", + "engelgabriel" + ] + }, + { + "pr": "13531", + "title": "Move mongo config away from cors package", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13529", + "title": "Regression: Add debounce on admin users search to avoid blocking by DDP Rate Limiter", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13523", + "title": "Remove Package references", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13518", + "title": "Remove Npm.depends and Npm.require except those that are inside package.js", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13515", + "title": "[FIX]Fix wrong this scope in Notifications", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13519", + "title": "Update Meteor 1.8.0.2", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13520", + "title": "Convert rc-nrr and slashcommands open to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13522", + "title": "[BREAK] Remove internal hubot package", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13485", + "title": "[FIX] Get next Livechat agent endpoint", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13491", + "title": "[IMPROVE] Add department field on find guest method", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow", + "MarcosSpessatto" + ] + }, + { + "pr": "13516", + "title": "Regression: Fix wrong imports in rc-models", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "13497", + "title": "Regression: Fix autolinker that was not parsing urls correctly", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13509", + "title": "Regression: Not updating subscriptions and not showing desktop notifcations", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13315", + "title": "[NEW] Add missing remove add leader channel", + "userLogin": "Montel", + "milestone": "1.0.0", + "contributors": [ + "Montel", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "13443", + "title": "[NEW] users.setActiveStatus endpoint in rest api", + "userLogin": "thayannevls", + "milestone": "1.0.0", + "contributors": [ + "thayannevls", + "web-flow", + "MarcosSpessatto" + ] + }, + { + "pr": "13422", + "title": " Fix some imports from wrong packages, remove exports and files unused in rc-ui", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13421", + "title": " Remove functions from globals", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13420", + "title": " Remove unused files and code in rc-lib - step 3", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13419", + "title": " Remove unused files in rc-lib - step 2", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13416", + "title": " Remove unused files and code in rc-lib - step 1", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13415", + "title": " Convert rocketchat-lib to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13496", + "title": "Regression: Message box geolocation was throwing error", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "13414", + "title": " Import missed functions to remove dependency of RC namespace", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13409", + "title": " Convert rocketchat-apps to main module structure", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13405", + "title": "Remove dependency of RC namespace in root server folder - step 6", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13402", + "title": "Remove dependency of RC namespace in root server folder - step 5", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13400", + "title": " Remove dependency of RC namespace in root server folder - step 4", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13398", + "title": "Remove dependency of RC namespace in root server folder - step 3", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13397", + "title": "Remove dependency of RC namespace in root server folder - step 2", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13390", + "title": " Remove dependency of RC namespace in root server folder - step 1", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13389", + "title": " Remove dependency of RC namespace in root client folder, imports/message-read-receipt and imports/personal-access-tokens", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13386", + "title": " Remove dependency of RC namespace in rc-integrations and importer-hipchat-enterprise", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13384", + "title": "Move rc-livechat server models to rc-models", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13383", + "title": " Remove dependency of RC namespace in rc-livechat/server/publications", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13382", + "title": "Remove dependency of RC namespace in rc-livechat/server/methods", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13379", + "title": "Remove dependency of RC namespace in rc-livechat/imports, lib, server/api, server/hooks and server/lib", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13378", + "title": " Remove LIvechat global variable from RC namespace", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13377", + "title": "Remove dependency of RC namespace in rc-livechat/server/models", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13370", + "title": " Remove dependency of RC namespace in livechat/client", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13492", + "title": "Remove dependency of RC namespace in rc-wordpress, chatpal-search and irc", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13366", + "title": " Remove dependency of RC namespace in rc-videobridge and webdav", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13365", + "title": " Remove dependency of RC namespace in rc-ui-master, ui-message- user-data-download and version-check", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13362", + "title": "Remove dependency of RC namespace in rc-ui-clean-history, ui-admin and ui-login", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13361", + "title": " Remove dependency of RC namespace in rc-ui, ui-account and ui-admin", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13359", + "title": " Remove dependency of RC namespace in rc-statistics and tokenpass", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13358", + "title": " Remove dependency of RC namespace in rc-smarsh-connector, sms and spotify", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13357", + "title": "Remove dependency of RC namespace in rc-slash-kick, leave, me, msg, mute, open, topic and unarchiveroom", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13356", + "title": " Remove dependency of RC namespace in rc-slash-archiveroom, create, help, hide, invite, inviteall and join", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13348", + "title": "Remove dependency of RC namespace in rc-setup-wizard, slackbridge and asciiarts", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13347", + "title": " Remove dependency of RC namespace in rc-reactions, retention-policy and search", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13345", + "title": " Remove dependency of RC namespace in rc-oembed and rc-otr", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13344", + "title": "Remove dependency of RC namespace in rc-oauth2-server and message-star", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13343", + "title": " Remove dependency of RC namespace in rc-message-pin and message-snippet", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13482", + "title": "[FIX] Sidenav mouse hover was slow", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "tassoevan" + ] + }, + { + "pr": "13483", + "title": "Depackaging", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok" + ] + }, + { + "pr": "13447", + "title": "[FIX] Emoji detection at line breaks", + "userLogin": "savish28", + "milestone": "1.0.0", + "contributors": [ + "savish28", + "web-flow" + ] + }, + { + "pr": "13471", + "title": "Room loading improvements", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13360", + "title": "[FIX] Invalid condition on getting next livechat agent over REST API endpoint", + "userLogin": "renatobecker", + "milestone": "0.74.3", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13417", + "title": "[IMPROVE] Open rooms quicker", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13457", + "title": "[FIX] \"Test Desktop Notifications\" not triggering a notification", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13463", + "title": "[FIX] Translated and incorrect i18n variables", + "userLogin": "leonboot", + "milestone": "0.74.3", + "contributors": [ + "leonboot", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13456", + "title": "Regression: Remove console.log on email translations", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13408", + "title": "[FIX] Properly escape custom emoji names for pattern matching", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13444", + "title": "[FIX] Small improvements on message box", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "11698", + "title": "[IMPROVE] KaTeX and Autolinker message rendering", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13452", + "title": "[FIX] Not translated emails", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13435", + "title": "Merge master into develop & Set version to 1.0.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "rodrigok", + "web-flow", + "graywolf336", + "theundefined", + "TkTech", + "MarcosSpessatto", + "geekgonecrazy", + "d-gubert", + "renatobecker", + "Hudell" + ] + }, + { + "pr": "13244", + "title": "[FIX] Update Russian localization", + "userLogin": "BehindLoader", + "milestone": "0.74.3", + "contributors": [ + "BehindLoader", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "13437", + "title": "[FIX] XML-decryption module not found", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "13436", + "title": "[IMPROVE] Allow configure Prometheus port per process via Environment Variable", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13430", + "title": "[IMPROVE] Add API option \"permissionsRequired\"", + "userLogin": "d-gubert", + "milestone": "0.74.3", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13336", + "title": "[FIX] Several Problems on HipChat Importer", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "Hudell", + "web-flow" + ] + }, + { + "pr": "13423", + "title": "[FIX] Invalid push gateway configuration, requires the uniqueId", + "userLogin": "graywolf336", + "milestone": "0.74.3", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "13396", + "title": "[IMPROVE] Update to MongoDB 4.0 in docker-compose file", + "userLogin": "ngulden", + "contributors": [ + "ngulden" + ] + }, + { + "pr": "7929", + "title": "[NEW] User avatars from external source", + "userLogin": "mjovanovic0", + "milestone": "1.0.0", + "contributors": [ + "mjovanovic0", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13411", + "title": "Regression: Table admin pages", + "userLogin": "ggazzo", + "contributors": [ + "tassoevan", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13410", + "title": "Regression: Template error", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "ggazzo" + ] + }, + { + "pr": "13407", + "title": "[FIX] Misaligned upload progress bar \"cancel\" button", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13406", + "title": "Removed old templates", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13393", + "title": "[IMPROVE] Admin ui", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "11451", + "title": "[FIX] Fixing rooms find by type and name", + "userLogin": "hmagarotto", + "milestone": "1.0.0", + "contributors": [ + "hmagarotto", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13363", + "title": "[FIX] linear-gradient background on safari", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13401", + "title": "[IMPROVE] End to end tests", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego", + "ggazzo" + ] + }, + { + "pr": "12380", + "title": "[IMPROVE] Update deleteUser errors to be more semantic", + "userLogin": "timkinnane", + "contributors": [ + "timkinnane", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "13369", + "title": "[FIX] Notify private settings changes even on public settings changed", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "11558", + "title": "[FIX] Fixed text for \"bulk-register-user\"", + "userLogin": "the4ndy", + "milestone": "1.0.0", + "contributors": [ + "the4ndy", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "13350", + "title": "[FIX] Pass token for cloud register", + "userLogin": "geekgonecrazy", + "milestone": "0.74.2", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "13342", + "title": "[IMPROVE] Send `uniqueID` to all clients so Jitsi rooms can be created correctly", + "userLogin": "sampaiodiego", + "milestone": "0.74.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13349", + "title": "[FIX] Setup wizard calling 'saveSetting' for each field/setting", + "userLogin": "ggazzo", + "milestone": "0.74.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "13326", + "title": "[FIX] Rate Limiter was limiting communication between instances", + "userLogin": "rodrigok", + "milestone": "0.74.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "11673", + "title": "[IMPROVE] Line height on static content pages", + "userLogin": "timkinnane", + "milestone": "1.0.0", + "contributors": [ + "timkinnane", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "13289", + "title": "[IMPROVE] new icons", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13311", + "title": "[NEW] Limit all DDP/Websocket requests (configurable via admin panel)", + "userLogin": "rodrigok", + "milestone": "0.74.1", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13322", + "title": "[FIX] Mobile view and re-enable E2E tests", + "userLogin": "sampaiodiego", + "milestone": "0.74.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13308", + "title": "[NEW] REST endpoint to forward livechat rooms", + "userLogin": "renatobecker", + "milestone": "0.74.1", + "contributors": [ + "renatobecker", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13293", + "title": "[FIX] Hipchat Enterprise Importer not generating subscriptions", + "userLogin": "Hudell", + "milestone": "0.74.1", + "contributors": [ + "Hudell", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13294", + "title": "[FIX] Message updating by Apps", + "userLogin": "sampaiodiego", + "milestone": "0.74.1", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13306", + "title": "[FIX] REST endpoint for creating custom emojis", + "userLogin": "sampaiodiego", + "milestone": "0.74.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13303", + "title": "[FIX] Preview of image uploads were not working when apps framework is enable", + "userLogin": "rodrigok", + "milestone": "0.74.1", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13221", + "title": "[FIX] HipChat Enterprise importer fails when importing a large amount of messages (millions)", + "userLogin": "Hudell", + "milestone": "0.74.1", + "contributors": [ + "Hudell", + "tassoevan" + ] + }, + { + "pr": "11525", + "title": "[NEW] Collect data for Monthly/Daily Active Users for a future dashboard", + "userLogin": "renatobecker", + "milestone": "0.74.1", + "contributors": [ + "renatobecker", + "rodrigok" + ] + }, + { + "pr": "13248", + "title": "[NEW] Add parseUrls field to the apps message converter", + "userLogin": "d-gubert", + "milestone": "0.74.1", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13282", + "title": "Fix: Missing export in cloud package", + "userLogin": "geekgonecrazy", + "milestone": "0.74.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "12341", + "title": "[FIX] Fix bug when user try recreate channel or group with same name and remove room from cache when user leaves room", + "userLogin": "MarcosSpessatto", + "milestone": "0.74.1", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13474", + "title": "Release 0.74.3", + "userLogin": "sampaiodiego", + "contributors": [ + "tassoevan", + "sampaiodiego", + "graywolf336", + "Hudell", + "d-gubert", + "rodrigok", + "BehindLoader", + "leonboot", + "renatobecker" + ] + }, + { + "pr": "13471", + "title": "Room loading improvements", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13360", + "title": "[FIX] Invalid condition on getting next livechat agent over REST API endpoint", + "userLogin": "renatobecker", + "milestone": "0.74.3", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13417", + "title": "[IMPROVE] Open rooms quicker", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13457", + "title": "[FIX] \"Test Desktop Notifications\" not triggering a notification", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13463", + "title": "[FIX] Translated and incorrect i18n variables", + "userLogin": "leonboot", + "milestone": "0.74.3", + "contributors": [ + "leonboot", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13456", + "title": "Regression: Remove console.log on email translations", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13408", + "title": "[FIX] Properly escape custom emoji names for pattern matching", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13452", + "title": "[FIX] Not translated emails", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13437", + "title": "[FIX] XML-decryption module not found", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "13244", + "title": "[FIX] Update Russian localization", + "userLogin": "BehindLoader", + "milestone": "0.74.3", + "contributors": [ + "BehindLoader", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "13436", + "title": "[IMPROVE] Allow configure Prometheus port per process via Environment Variable", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13430", + "title": "[IMPROVE] Add API option \"permissionsRequired\"", + "userLogin": "d-gubert", + "milestone": "0.74.3", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13336", + "title": "[FIX] Several Problems on HipChat Importer", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "Hudell", + "web-flow" + ] + }, + { + "pr": "13423", + "title": "[FIX] Invalid push gateway configuration, requires the uniqueId", + "userLogin": "graywolf336", + "milestone": "0.74.3", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "13369", + "title": "[FIX] Notify private settings changes even on public settings changed", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13407", + "title": "[FIX] Misaligned upload progress bar \"cancel\" button", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.0.0-rc.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13815", + "title": "[NEW] Add an option to delete file in files list", + "userLogin": "marceloschmidt", + "milestone": "1.0.0", + "contributors": [ + "marceloschmidt", + "engelgabriel", + "web-flow", + "sampaiodiego", + "d-gubert" + ] + }, + { + "pr": "13996", + "title": "[NEW] Threads V 1.0", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "12834", + "title": "Add pagination to getUsersOfRoom", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow", + "engelgabriel", + "sampaiodiego", + "Hudell", + "rodrigok" + ] + }, + { + "pr": "13925", + "title": "OpenShift custom OAuth support", + "userLogin": "bsharrow", + "milestone": "1.0.0", + "contributors": [ + "bsharrow", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "14026", + "title": "Settings: disable reset button", + "userLogin": "alansikora", + "milestone": "1.0.0", + "contributors": [ + "alansikora", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "14025", + "title": "Settings: hiding reset button for readonly fields", + "userLogin": "alansikora", + "milestone": "1.0.0", + "contributors": [ + "alansikora", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13510", + "title": "[NEW] Add support to updatedSince parameter in emoji-custom.list and deprecated old endpoint", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13884", + "title": "[IMPROVE] Add permission to change other user profile avatar", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "marceloschmidt", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "13732", + "title": "[IMPROVE] UI of Permissions page", + "userLogin": "fliptrail", + "milestone": "1.0.0", + "contributors": [ + "fliptrail", + "engelgabriel", + "web-flow", + "marceloschmidt" + ] + }, + { + "pr": "13829", + "title": "[NEW] Chatpal: Enable custom search parameters", + "userLogin": "Peym4n", + "milestone": "1.0.0", + "contributors": [ + "Peym4n", + "web-flow" + ] + }, + { + "pr": "13842", + "title": "[FIX] Closing sidebar when room menu is clicked.", + "userLogin": "Kailash0311", + "milestone": "1.0.0", + "contributors": [ + "Kailash0311", + "sampaiodiego", + "web-flow", + "engelgabriel", + "rodrigok" + ] + }, + { + "pr": "14021", + "title": "[FIX] Check settings for name requirement before validating", + "userLogin": "marceloschmidt", + "contributors": [ + "marceloschmidt", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13979", + "title": "Fix debug logging not being enabled by the setting", + "userLogin": "graywolf336", + "milestone": "1.0.0", + "contributors": [ + "graywolf336", + "geekgonecrazy", + "web-flow", + "engelgabriel", + "rodrigok" + ] + }, + { + "pr": "13982", + "title": "[FIX] Links and upload paths when running in a subdir", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13532", + "title": "[FIX] users.getPreferences when the user doesn't have any preferences", + "userLogin": "thayannevls", + "milestone": "1.0.0", + "contributors": [ + "thayannevls" + ] + }, + { + "pr": "13495", + "title": "[FIX] Real names were not displayed in the reactions (API/UI)", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "tassoevan", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "13798", + "title": "Deprecate /api/v1/info in favor of /api/info", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13776", + "title": "Change dynamic dependency of FileUpload in Messages models", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "rodrigok", + "web-flow", + "engelgabriel" + ] + }, + { + "pr": "14017", + "title": "Allow set env var METEOR_OPLOG_TOO_FAR_BEHIND", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "geekgonecrazy", + "web-flow", + "engelgabriel" + ] + }, + { + "pr": "14015", + "title": "[FIX] Theme CSS loading in subdir env", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13250", + "title": "[FIX] Fix rendering of links in the announcement modal", + "userLogin": "supra08", + "milestone": "1.0.0", + "contributors": [ + "supra08", + "tassoevan" + ] + }, + { + "pr": "13791", + "title": "[IMPROVE] Use SessionId for credential token in SAML request", + "userLogin": "MohammedEssehemy", + "milestone": "1.0.0", + "contributors": [ + "MohammedEssehemy", + "web-flow", + "engelgabriel" + ] + }, + { + "pr": "13969", + "title": "[FIX] Add custom MIME types for *.ico extension", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13994", + "title": "[FIX] Groups endpoints permission validations", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13981", + "title": "[FIX] Focus on input when emoji picker box is open was not working", + "userLogin": "d-gubert", + "milestone": "1.0.0", + "contributors": [ + "d-gubert", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "13984", + "title": "Improve: Decrease padding for app buy modal", + "userLogin": "geekgonecrazy", + "milestone": "1.0.0", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "13983", + "title": "[NEW] - Add setting to request a comment when closing Livechat room", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13824", + "title": "[FIX] Auto hide Livechat room from sidebar on close", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "13927", + "title": "[BREAK] Prevent start if incompatible mongo version", + "userLogin": "geekgonecrazy", + "milestone": "1.0.0", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "13820", + "title": "[FIX] Improve cloud section", + "userLogin": "geekgonecrazy", + "milestone": "1.0.0", + "contributors": [ + "geekgonecrazy", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13746", + "title": "[FIX] Wrong permalink when running in subdir", + "userLogin": "ura14h", + "milestone": "1.0.0", + "contributors": [ + "ura14h", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13968", + "title": "[FIX] Change localStorage keys to work when server is running in a subdir", + "userLogin": "MarcosSpessatto", + "contributors": [ + "MarcosSpessatto" + ] + } + ] + }, + "1.0.0-rc.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14057", + "title": "Prioritize user-mentions badge", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14047", + "title": "[IMPROVE] Include more information to help with bug reports and debugging", + "userLogin": "geekgonecrazy", + "milestone": "1.0.0", + "contributors": [ + "geekgonecrazy", + "sampaiodiego" + ] + }, + { + "pr": "14030", + "title": "[IMPROVE] New sidebar item badges, mention links, and ticks", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14049", + "title": "Proper thread quote, clear message box on send, and other nice things to have", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14054", + "title": "Fix: Tests were not exiting RC instances", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14048", + "title": "Fix shield indentation", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14052", + "title": "Fix modal scroll", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14041", + "title": "Fix race condition of lastMessage set", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14044", + "title": "Fix room re-rendering", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "14043", + "title": "Fix sending notifications to mentions on threads and discussion email sender", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14018", + "title": "Fix discussions issues after room deletion and translation actions not being shown", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow", + "engelgabriel", + "sampaiodiego" + ] + } + ] + }, + "1.0.0-rc.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14053", + "title": "Show discussion avatar", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "d-gubert", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14179", + "title": "[FIX] SAML certificate settings don't follow a pattern", + "userLogin": "Hudell", + "milestone": "1.0.0", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14180", + "title": "Fix threads tests", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14160", + "title": "Prevent error for ldap login with invalid characters", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13992", + "title": "[IMPROVE] Remove setting to show a livechat is waiting", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow", + "engelgabriel", + "sampaiodiego" + ] + }, + { + "pr": "14174", + "title": "[REGRESSION] Messages sent by livechat's guests are losing sender info", + "userLogin": "d-gubert", + "milestone": "1.0.0", + "contributors": [ + "d-gubert", + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14045", + "title": "[NEW] Rest threads", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14171", + "title": "Faster CI build for PR", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14161", + "title": "Regression: Message box does not go back to initial state after sending a message", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "tassoevan" + ] + }, + { + "pr": "14170", + "title": "Prevent error on normalize thread message for preview", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14137", + "title": "[IMPROVE] Attachment download caching", + "userLogin": "wreiske", + "milestone": "1.0.0", + "contributors": [ + "wreiske", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14147", + "title": "[NEW] Add GET method to fetch Livechat message through REST API", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14121", + "title": "[FIX] Custom Oauth store refresh and id tokens with expiresIn", + "userLogin": "ralfbecker", + "contributors": [ + "ralfbecker", + "geekgonecrazy", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14071", + "title": "Update badges and mention links colors", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "14028", + "title": "[FIX] Apps converters delete fields on message attachments", + "userLogin": "d-gubert", + "milestone": "1.0.0", + "contributors": [ + "d-gubert", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14131", + "title": "[IMPROVE] Get avatar from oauth", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "13761", + "title": "[IMPROVE] OAuth Role Sync", + "userLogin": "hypery2k", + "contributors": [ + "hypery2k", + "engelgabriel", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "14113", + "title": "[FIX] Custom Oauth login not working with accessToken", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14099", + "title": "Smaller thread replies and system messages", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "sampaiodiego", + "web-flow", + "rodrigok", + "engelgabriel" + ] + }, + { + "pr": "14148", + "title": "[FIX] renderField template to correct short property usage", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "14129", + "title": "[FIX] Updating a message from apps if keep history is on", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13697", + "title": "[NEW] Add Voxtelesys to list of SMS providers", + "userLogin": "john08burke", + "milestone": "1.0.0", + "contributors": [ + "jhnburke8", + "engelgabriel", + "web-flow", + "john08burke", + "sampaiodiego" + ] + }, + { + "pr": "14130", + "title": "[FIX] Missing connection headers on Livechat REST API", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "14125", + "title": "Regression: User autocomplete was not listing users from correct room", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14097", + "title": "Regression: Role creation and deletion error fixed", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14111", + "title": "[Regression] Fix integrations message example", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14118", + "title": "Fix update apps capability of updating messages", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14100", + "title": "Fix: Skip thread notifications on message edit", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14116", + "title": "Fix: Remove message class `sequential` if `new-day` is present", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14103", + "title": "[FIX] Receiving agent for new livechats from REST API", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14102", + "title": "Fix top bar unread message counter", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13987", + "title": "[NEW] Rest endpoints of discussions", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "engelgabriel", + "web-flow", + "rodrigok", + "d-gubert" + ] + }, + { + "pr": "10695", + "title": "[FIX] Livechat user registration in another department", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "sampaiodiego", + "web-flow", + "ggazzo", + "engelgabriel" + ] + }, + { + "pr": "14046", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14074", + "title": "[FIX] Support for handling SAML LogoutRequest SLO", + "userLogin": "geekgonecrazy", + "milestone": "1.0.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14101", + "title": "Fix sending message from action buttons in messages", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14089", + "title": "Fix: Error when version check endpoint was returning invalid data", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14066", + "title": "Wait port release to finish tests", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14059", + "title": "Fix threads rendering performance", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan", + "sampaiodiego" + ] + }, + { + "pr": "14076", + "title": "Unstuck observers every minute", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14031", + "title": "[FIX] Livechat office hours", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14051", + "title": "Fix messages losing thread titles on editing or reaction and improve message actions", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14072", + "title": "[IMPROVE] Update the Apps Engine version to v1.4.1", + "userLogin": "graywolf336", + "contributors": [ + "graywolf336" + ] + } + ] + }, + "1.0.0-rc.4": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14262", + "title": "[FIX] Auto-translate toggle not updating rendered messages", + "userLogin": "marceloschmidt", + "contributors": [ + "marceloschmidt", + "web-flow" + ] + }, + { + "pr": "14265", + "title": "[FIX] Align burger menu in header with content matching room header", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14266", + "title": "Improve message validation", + "userLogin": "d-gubert", + "milestone": "1.0.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "14007", + "title": "Added federation ping, loopback and dashboard", + "userLogin": "alansikora", + "contributors": [ + "alansikora", + "rodrigok" + ] + }, + { + "pr": "14012", + "title": "[FIX] Normalize TAPi18n language string on Livechat widget", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14163", + "title": "[FIX] Autogrow not working properly for many message boxes", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14207", + "title": "[FIX] Image attachment re-renders on message update", + "userLogin": "Kailash0311", + "milestone": "1.0.0", + "contributors": [ + "Kailash0311", + "web-flow" + ] + }, + { + "pr": "14251", + "title": "Regression: Exception on notification when adding someone in room via mention", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14246", + "title": "Regression: fix grouping for reactive message", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "11346", + "title": "[NEW] Multiple slackbridges", + "userLogin": "Hudell", + "milestone": "1.0.0", + "contributors": [ + "kable-wilmoth", + "Hudell", + "web-flow", + "engelgabriel" + ] + }, + { + "pr": "14010", + "title": "[FIX] Sidenav does not open on some admin pages", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto", + "tassoevan" + ] + }, + { + "pr": "14245", + "title": "Regression: Cursor position set to beginning when editing a message", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "tassoevan" + ] + }, + { + "pr": "14244", + "title": "[FIX] Empty result when getting badge count notification", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14224", + "title": "[NEW] option to not use nrr (experimental)", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14238", + "title": "Regression: grouping messages on threads", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14236", + "title": "[NEW]Set up livechat connections created from new client", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14235", + "title": "Regression: Remove border from unstyled message body", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14234", + "title": "Move LDAP Escape to login handler", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14227", + "title": "[BREAK] Require OPLOG/REPLICASET to run Rocket.Chat", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14216", + "title": "[Regression] Personal Access Token list fixed", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14226", + "title": "ESLint: Add more import rules", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14225", + "title": "Regression: fix drop file", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14222", + "title": "Broken styles in Administration's contextual bar", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14223", + "title": "Regression: Broken UI for messages", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14220", + "title": "Exit process on unhandled rejection", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14217", + "title": "Unify mime-type package configuration", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14219", + "title": "Regression: Prevent startup errors for mentions parsing", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14189", + "title": "Regression: System messages styling", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14214", + "title": "[NEW] allow drop files on thread", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "14188", + "title": "[FIX] Obey audio notification preferences", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14215", + "title": "Prevent click on reply thread to trigger flex tab closing", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14177", + "title": "created function to allow change default values, fix loading search users", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14213", + "title": "Use main message as thread tab title", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14210", + "title": "Use own logic to get thread infos via REST", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14192", + "title": "Regression: wrong expression at messageBox.actions.remove()", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14185", + "title": "Increment user counter on DMs", + "userLogin": "sampaiodiego", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14184", + "title": "[REGRESSION] Fix variable name references in message template", + "userLogin": "d-gubert", + "milestone": "1.0.0", + "contributors": [ + "d-gubert" + ] + } + ] + }, + "1.0.0-rc.5": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14276", + "title": "Regression: Active room was not being marked", + "userLogin": "rodrigok", + "milestone": "1.0.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14211", + "title": "Rename Cloud to Connectivity Services & split Apps in Apps and Marketplace", + "userLogin": "geekgonecrazy", + "milestone": "1.0.0", + "contributors": [ + "geekgonecrazy", + "engelgabriel", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14178", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "milestone": "1.0.0", + "contributors": [ + "sampaiodiego", + "rodrigok" + ] + }, + { + "pr": "13986", + "title": "[IMPROVE] Replace livechat inquiry dialog with preview room", + "userLogin": "renatobecker", + "milestone": "1.0.0", + "contributors": [ + "renatobecker", + "web-flow", + "engelgabriel", + "sampaiodiego", + "tassoevan" + ] + }, + { + "pr": "14050", + "title": "Regression: Discussions were not showing on Tab Bar", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "tassoevan" + ] + }, + { + "pr": "14274", + "title": "Force unstyling of blockquote under .message-body--unstyled", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14273", + "title": "[FIX] Slackbridge private channels", + "userLogin": "Hudell", + "milestone": "1.0.0", + "contributors": [ + "nylen", + "web-flow", + "MarcosSpessatto", + "sampaiodiego", + "Hudell" + ] + }, + { + "pr": "14081", + "title": "[FIX] View All members button now not in direct room", + "userLogin": "knrt10", + "milestone": "1.0.0", + "contributors": [ + "knrt10", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14229", + "title": "Regression: Admin embedded layout", + "userLogin": "tassoevan", + "milestone": "1.0.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14268", + "title": "[NEW] Update message actions", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14269", + "title": "New threads layout", + "userLogin": "ggazzo", + "milestone": "1.0.0", + "contributors": [ + "ggazzo", + "rodrigok" + ] + }, + { + "pr": "14258", + "title": "Improve: Marketplace auth inside Rocket.Chat instead of inside the iframe. ", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14150", + "title": "[New] Reply privately to group messages", + "userLogin": "bhardwajaditya", + "milestone": "1.0.0", + "contributors": [ + "bhardwajaditya", + "engelgabriel", + "web-flow", + "MarcosSpessatto" + ] + } + ] + }, + "1.0.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13474", + "title": "Release 0.74.3", + "userLogin": "sampaiodiego", + "contributors": [ + "tassoevan", + "sampaiodiego", + "graywolf336", + "Hudell", + "d-gubert", + "rodrigok", + "BehindLoader", + "leonboot", + "renatobecker" + ] + }, + { + "pr": "13471", + "title": "Room loading improvements", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13360", + "title": "[FIX] Invalid condition on getting next livechat agent over REST API endpoint", + "userLogin": "renatobecker", + "milestone": "0.74.3", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "13417", + "title": "[IMPROVE] Open rooms quicker", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "13457", + "title": "[FIX] \"Test Desktop Notifications\" not triggering a notification", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13463", + "title": "[FIX] Translated and incorrect i18n variables", + "userLogin": "leonboot", + "milestone": "0.74.3", + "contributors": [ + "leonboot", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "13456", + "title": "Regression: Remove console.log on email translations", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13408", + "title": "[FIX] Properly escape custom emoji names for pattern matching", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13452", + "title": "[FIX] Not translated emails", + "userLogin": "sampaiodiego", + "milestone": "0.74.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13437", + "title": "[FIX] XML-decryption module not found", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "13244", + "title": "[FIX] Update Russian localization", + "userLogin": "BehindLoader", + "milestone": "0.74.3", + "contributors": [ + "BehindLoader", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "13436", + "title": "[IMPROVE] Allow configure Prometheus port per process via Environment Variable", + "userLogin": "rodrigok", + "milestone": "0.74.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "13430", + "title": "[IMPROVE] Add API option \"permissionsRequired\"", + "userLogin": "d-gubert", + "milestone": "0.74.3", + "contributors": [ + "d-gubert", + "web-flow" + ] + }, + { + "pr": "13336", + "title": "[FIX] Several Problems on HipChat Importer", + "userLogin": "Hudell", + "milestone": "0.74.3", + "contributors": [ + "rodrigok", + "Hudell", + "web-flow" + ] + }, + { + "pr": "13423", + "title": "[FIX] Invalid push gateway configuration, requires the uniqueId", + "userLogin": "graywolf336", + "milestone": "0.74.3", + "contributors": [ + "graywolf336" + ] + }, + { + "pr": "13369", + "title": "[FIX] Notify private settings changes even on public settings changed", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "13407", + "title": "[FIX] Misaligned upload progress bar \"cancel\" button", + "userLogin": "tassoevan", + "milestone": "0.74.3", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.0.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14296", + "title": "[FIX] Popup cloud console in new window", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "14288", + "title": "[FIX] Switch oplog required doc link to more accurate link", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14291", + "title": "[FIX] Optional exit on Unhandled Promise Rejection", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14293", + "title": "[FIX] Error when accessing avatar with no token", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "rodrigok" + ] + }, + { + "pr": "14286", + "title": "[FIX] Startup error in registration check", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14290", + "title": "[FIX] Wrong header at Apps admin section", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14282", + "title": "[FIX] Error when accessing an invalid file upload url", + "userLogin": "wreiske", + "milestone": "1.0.1", + "contributors": [ + "wreiske", + "d-gubert", + "web-flow" + ] + } + ] + }, + "1.0.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14339", + "title": "Release 1.0.2", + "userLogin": "rodrigok", + "contributors": [ + "tassoevan", + "rodrigok", + "mohamedar97", + "ggazzo", + "thaiphv", + "knrt10", + "lolimay", + "AnBo83", + "sampaiodiego" + ] + }, + { + "pr": "14338", + "title": "[FIX] Unread line and new day separator were not aligned ", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14336", + "title": "[FIX] Audio notification for messages on DM", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14334", + "title": "[IMPROVE] i18n of threads and discussion buttons", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14330", + "title": "[FIX] Duplicate thread message after editing", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14328", + "title": "[FIX] New day separator rendered over thread reply", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14320", + "title": "[IMPROVE] Better error message when not able to get MongoDB Version", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14318", + "title": "Add cross-browser select arrow positioning", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14182", + "title": "i18n: Update German strings", + "userLogin": "AnBo83", + "milestone": "1.0.2", + "contributors": [ + "AnBo83", + "web-flow" + ] + }, + { + "pr": "14011", + "title": "[FIX] Missing i18n for some new Permissions", + "userLogin": "lolimay", + "milestone": "1.0.2", + "contributors": [ + "lolimay", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14316", + "title": "[FIX] View Logs admin page was broken and not rendering color logs", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14301", + "title": "[Regression] Anonymous user fix", + "userLogin": "knrt10", + "milestone": "1.0.2", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14299", + "title": "Coerces the MongoDB version string", + "userLogin": "thaiphv", + "milestone": "1.0.2", + "contributors": [ + "thaiphv" + ] + }, + { + "pr": "14313", + "title": "[FIX] show roles on message", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14075", + "title": "[Fix] group name appears instead of the room id", + "userLogin": "mohamedar97", + "milestone": "1.0.2", + "contributors": [ + "mohamedar97" + ] + }, + { + "pr": "14311", + "title": "[FIX] Remove reference to inexistent field when deleting message in thread", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.0.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14446", + "title": "Release 1.0.3", + "userLogin": "rodrigok", + "milestone": "1.0.3", + "contributors": [ + "rodrigok", + "mrsimpson", + "ggazzo", + "MarcosSpessatto", + "geekgonecrazy", + "tassoevan", + "engelgabriel", + "sampaiodiego" + ] + } + ] + }, + "1.1.0-rc.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14252", + "title": "[IMPROVE] Message rendering time", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14488", + "title": "[IMPROVE] Change user presence events to Meteor Streams", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "13641", + "title": "Removed unnecessary DDP unblocks", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14485", + "title": "[FIX] Downloading files when running in sub directory", + "userLogin": "miolane", + "milestone": "1.1.0", + "contributors": [ + null, + "miolane", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "14567", + "title": "[FIX] Broken layout when sidebar is open on IE/Edge", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14542", + "title": "[FIX] Channel names on Directory got cut on small screens", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14505", + "title": "[FIX] Duplicated link to jump to message ", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14369", + "title": "[FIX] Edit Message when down arrow is pressed.", + "userLogin": "Kailash0311", + "milestone": "1.1.0", + "contributors": [ + "Kailash0311", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14570", + "title": "Fix emoji replacing some chars", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "13807", + "title": "[IMPROVE] Upgrade EmojiOne to JoyPixels 4.5.0", + "userLogin": "wreiske", + "milestone": "1.1.0", + "contributors": [ + "wreiske", + "web-flow", + "sampaiodiego", + "engelgabriel" + ] + }, + { + "pr": "13919", + "title": "[FIX] Unread property of the room's lastMessage object was being wrong some times", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14561", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "10273", + "title": "[NEW] Setting option to mark as containing a secret/password", + "userLogin": "kb0304", + "milestone": "1.1.0", + "contributors": [ + "geekgonecrazy", + "web-flow", + "MarcosSpessatto" + ] + }, + { + "pr": "13736", + "title": "Refactor WebRTC class", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow", + "rodrigok", + "sampaiodiego" + ] + }, + { + "pr": "14551", + "title": "Update Meteor Streamer package", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "12084", + "title": "[FIX] Multiple Slack Importer Bugs", + "userLogin": "Hudell", + "milestone": "1.1.0", + "contributors": [ + "Hudell", + "web-flow" + ] + }, + { + "pr": "14534", + "title": "[FIX] No feedback when adding users that already exists in a room", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "gsunit", + "web-flow", + "MarcosSpessatto" + ] + }, + { + "pr": "14543", + "title": "Regression: unit tests were being skipped", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14516", + "title": "[FIX] Custom scripts descriptions were not clear enough ", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14263", + "title": "[FIX] Role `user` has being added after email verification even for non anonymous users", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14495", + "title": "MsgTyping refactor", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "11311", + "title": "[FIX] Several problems with read-only rooms and muted users", + "userLogin": "Hudell", + "milestone": "1.1.0", + "contributors": [ + "Hudell", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "13328", + "title": "[FIX] Channel settings form to textarea for Topic and Description", + "userLogin": "supra08", + "milestone": "1.1.0", + "contributors": [ + "supra08", + "ggazzo" + ] + }, + { + "pr": "12971", + "title": "[IMPROVE] Don't show unread count badge in burger menu if it is from the opened room", + "userLogin": "tassoevan", + "milestone": "1.1.0", + "contributors": [ + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "14503", + "title": "Google Plus account is no longer accessible", + "userLogin": "zdumitru", + "contributors": [ + "zdumitru", + "web-flow" + ] + }, + { + "pr": "13640", + "title": "[FIX] Elements in User Info require some padding", + "userLogin": "mushroomgenie", + "milestone": "1.1.0", + "contributors": [ + "mushroomgenie", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "13660", + "title": "[FIX] Showing the id instead of the name of custom notification sound", + "userLogin": "knrt10", + "milestone": "1.1.0", + "contributors": [ + "knrt10", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "13872", + "title": "[IMPROVEMENT] Add tooltip to to notify user the purpose of back button in discussion", + "userLogin": "ashwaniYDV", + "milestone": "1.1.0", + "contributors": [ + "ashwaniYDV", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14518", + "title": "eslint errors currently on develop", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14022", + "title": "[IMPROVE] Livechat CRM secret token optional", + "userLogin": "renatobecker", + "milestone": "1.1.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14509", + "title": "[FIX] Remove Livechat guest data was removing more rooms than expected", + "userLogin": "renatobecker", + "milestone": "1.1.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14456", + "title": "[FIX] Save custom emoji with special characters causes some errors", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14242", + "title": "[FIX] Verify if the user is requesting your own information in users.info", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "13682", + "title": "Allow removing description, topic and annoucement of rooms(set as empty string)", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14381", + "title": "[NEW] Custom user name field from Custom OAuth", + "userLogin": "mjovanovic0", + "milestone": "1.1.0", + "contributors": [ + "mjovanovic0", + "web-flow", + "geekgonecrazy" + ] + }, + { + "pr": "13615", + "title": "[NEW] Add pause and reset button when adding custom sound ", + "userLogin": "knrt10", + "milestone": "1.1.0", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14496", + "title": "[FIX] RocketChat client sending out video call requests unnecessarily", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14490", + "title": "[FIX] `Alphabetical` translation in DE", + "userLogin": "AnBo83", + "contributors": [ + "AnBo83", + "web-flow" + ] + }, + { + "pr": "13680", + "title": "[NEW] Missing \"view-outside-room_description\" translation key", + "userLogin": "bhardwajaditya", + "contributors": [ + "bhardwajaditya" + ] + }, + { + "pr": "14434", + "title": "[FIX] Fix redirect to First channel after login", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14257", + "title": "[IMPROVEMENT] Don't group messages with different alias", + "userLogin": "jungeonkim", + "milestone": "1.1.0", + "contributors": [ + "jungeonkim", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14460", + "title": "[IMPROVE] jump to selected message on open thread", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14465", + "title": "[FIX] Ignored messages", + "userLogin": "tassoevan", + "milestone": "1.1.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14478", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14464", + "title": "[FIX] Allow data URLs in isURL/getURL helpers", + "userLogin": "tassoevan", + "milestone": "1.1.0", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14461", + "title": "[FIX] You must join to view messages in this channel", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14459", + "title": "Remove specific eslint rules", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14332", + "title": "New eslint rules", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14443", + "title": "[FIX] Channel Leader Bar is in the way of Thread Header ", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14433", + "title": "Fix i18n files keys sort", + "userLogin": "sampaiodiego", + "milestone": "1.0.3", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14442", + "title": "[FIX] Discussion name being invalid", + "userLogin": "sampaiodiego", + "milestone": "1.0.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14415", + "title": "[FIX] Room name was undefined in some info dialogs", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.3", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14405", + "title": "[FIX] Exception on crowd sync due to a wrong logging method", + "userLogin": "rodrigok", + "milestone": "1.0.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14418", + "title": "Fixes on DAU and MAU aggregations", + "userLogin": "rodrigok", + "milestone": "1.0.3", + "contributors": [ + "rodrigok", + "web-flow" + ] + }, + { + "pr": "14397", + "title": "[IMPROVE] Don't use regex to find users", + "userLogin": "sampaiodiego", + "milestone": "1.0.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14431", + "title": "[IMPROVE] Added flag `skipActiveUsersToBeReady` to not wait the load of `active users` to present the Web interface", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "14422", + "title": "[FIX] IE11 support", + "userLogin": "tassoevan", + "milestone": "1.0.3", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14386", + "title": "Add missing german translations", + "userLogin": "mrsimpson", + "milestone": "1.0.3", + "contributors": [ + "mrsimpson", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14432", + "title": "[FIX] Escape unrecognized slash command message", + "userLogin": "tassoevan", + "milestone": "1.0.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14430", + "title": "[FIX] Mentions message missing 'jump to message' action", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14426", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "milestone": "1.0.3", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14419", + "title": "[FIX] preview pdf its not working", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "12891", + "title": "[IMPROVE] SAML login process refactoring", + "userLogin": "kukkjanos", + "milestone": "1.1.0", + "contributors": [ + "kukkjanos", + "web-flow" + ] + }, + { + "pr": "14404", + "title": "[FIX] Messages on thread panel were receiving wrong context/subscription", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14402", + "title": "[FIX] Error 400 on send a reply to an old thread", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14400", + "title": "[FIX] Users actions in administration were returning error", + "userLogin": "tassoevan", + "milestone": "1.0.3", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14403", + "title": "[FIX] Fallback to mongo version that doesn't require clusterMonitor role", + "userLogin": "geekgonecrazy", + "milestone": "1.0.3", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "14345", + "title": "[FIX] SAML credentialToken removal was preventing mobile from being able to authenticate", + "userLogin": "geekgonecrazy", + "milestone": "1.0.3", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14376", + "title": "[FIX] Stream not connecting connect when using subdir and multi-instance", + "userLogin": "geekgonecrazy", + "milestone": "1.0.3", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "14388", + "title": "[FIX] Pressing Enter in User Search field at channel causes reload", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.3", + "contributors": [ + "MarcosSpessatto", + "tassoevan" + ] + }, + { + "pr": "14379", + "title": "[FIX] Wrong token name was generating error on Gitlab OAuth login", + "userLogin": "MarcosSpessatto", + "milestone": "1.0.3", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14039", + "title": "fix discussions: remove restriction for editing room info, server side", + "userLogin": "mrsimpson", + "milestone": "1.1.0", + "contributors": [ + "mrsimpson", + "tassoevan", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "14387", + "title": "[FIX] more message actions to threads context(follow, unfollow, copy, delete)", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14396", + "title": "[FIX] Unnecessary meteor.defer on openRoom", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14389", + "title": "[IMPROVE] Allow change Discussion's properties", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14390", + "title": "Fix: Message body was not being updated when user disabled nrr message", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14393", + "title": "[FIX] Messages on threads disappearing", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14394", + "title": "[FIX] Bell was too small on threads", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14372", + "title": "[FIX] Main thread title on replies", + "userLogin": "ggazzo", + "milestone": "1.0.3", + "contributors": [ + "ggazzo", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14370", + "title": "[NEW] Returns custom emojis through the Livechat REST API", + "userLogin": "renatobecker", + "milestone": "1.1.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "14351", + "title": "Improve German translations", + "userLogin": "mrsimpson", + "milestone": "1.0.3", + "contributors": [ + "mrsimpson" + ] + }, + { + "pr": "14362", + "title": "[FIX] New day separator overlapping above system message", + "userLogin": "rodrigok", + "milestone": "1.0.3", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14338", + "title": "[FIX] Unread line and new day separator were not aligned ", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14336", + "title": "[FIX] Audio notification for messages on DM", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14334", + "title": "[IMPROVE] i18n of threads and discussion buttons", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14330", + "title": "[FIX] Duplicate thread message after editing", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14328", + "title": "[FIX] New day separator rendered over thread reply", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14320", + "title": "[IMPROVE] Better error message when not able to get MongoDB Version", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14318", + "title": "Add cross-browser select arrow positioning", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14182", + "title": "i18n: Update German strings", + "userLogin": "AnBo83", + "milestone": "1.0.2", + "contributors": [ + "AnBo83", + "web-flow" + ] + }, + { + "pr": "14011", + "title": "[FIX] Missing i18n for some new Permissions", + "userLogin": "lolimay", + "milestone": "1.0.2", + "contributors": [ + "lolimay", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14316", + "title": "[FIX] View Logs admin page was broken and not rendering color logs", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14317", + "title": "Merge master into develop & Set version to 1.1.0-develop", + "userLogin": "rodrigok", + "contributors": [ + "wreiske", + "rodrigok", + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14301", + "title": "[Regression] Anonymous user fix", + "userLogin": "knrt10", + "milestone": "1.0.2", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14299", + "title": "Coerces the MongoDB version string", + "userLogin": "thaiphv", + "milestone": "1.0.2", + "contributors": [ + "thaiphv" + ] + }, + { + "pr": "14313", + "title": "[FIX] show roles on message", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14075", + "title": "[Fix] group name appears instead of the room id", + "userLogin": "mohamedar97", + "milestone": "1.0.2", + "contributors": [ + "mohamedar97" + ] + }, + { + "pr": "14311", + "title": "[FIX] Remove reference to inexistent field when deleting message in thread", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14296", + "title": "[FIX] Popup cloud console in new window", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy" + ] + }, + { + "pr": "14288", + "title": "[FIX] Switch oplog required doc link to more accurate link", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14291", + "title": "[FIX] Optional exit on Unhandled Promise Rejection", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14293", + "title": "[FIX] Error when accessing avatar with no token", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "rodrigok" + ] + }, + { + "pr": "14286", + "title": "[FIX] Startup error in registration check", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14290", + "title": "[FIX] Wrong header at Apps admin section", + "userLogin": "geekgonecrazy", + "milestone": "1.0.1", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + }, + { + "pr": "14294", + "title": "Merge master into develop & Set version to 1.1.0-develop", + "userLogin": "rodrigok", + "contributors": [ + "sampaiodiego", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "14282", + "title": "[FIX] Error when accessing an invalid file upload url", + "userLogin": "wreiske", + "milestone": "1.0.1", + "contributors": [ + "wreiske", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14446", + "title": "Release 1.0.3", + "userLogin": "rodrigok", + "milestone": "1.0.3", + "contributors": [ + "rodrigok", + "mrsimpson", + "ggazzo", + "MarcosSpessatto", + "geekgonecrazy", + "tassoevan", + "engelgabriel", + "sampaiodiego" + ] + }, + { + "pr": "14339", + "title": "Release 1.0.2", + "userLogin": "rodrigok", + "contributors": [ + "tassoevan", + "rodrigok", + "mohamedar97", + "ggazzo", + "thaiphv", + "knrt10", + "lolimay", + "AnBo83", + "sampaiodiego" + ] + }, + { + "pr": "14338", + "title": "[FIX] Unread line and new day separator were not aligned ", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14336", + "title": "[FIX] Audio notification for messages on DM", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14334", + "title": "[IMPROVE] i18n of threads and discussion buttons", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14330", + "title": "[FIX] Duplicate thread message after editing", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14328", + "title": "[FIX] New day separator rendered over thread reply", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14320", + "title": "[IMPROVE] Better error message when not able to get MongoDB Version", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14318", + "title": "Add cross-browser select arrow positioning", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14182", + "title": "i18n: Update German strings", + "userLogin": "AnBo83", + "milestone": "1.0.2", + "contributors": [ + "AnBo83", + "web-flow" + ] + }, + { + "pr": "14011", + "title": "[FIX] Missing i18n for some new Permissions", + "userLogin": "lolimay", + "milestone": "1.0.2", + "contributors": [ + "lolimay", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14316", + "title": "[FIX] View Logs admin page was broken and not rendering color logs", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14301", + "title": "[Regression] Anonymous user fix", + "userLogin": "knrt10", + "milestone": "1.0.2", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14299", + "title": "Coerces the MongoDB version string", + "userLogin": "thaiphv", + "milestone": "1.0.2", + "contributors": [ + "thaiphv" + ] + }, + { + "pr": "14313", + "title": "[FIX] show roles on message", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14075", + "title": "[Fix] group name appears instead of the room id", + "userLogin": "mohamedar97", + "milestone": "1.0.2", + "contributors": [ + "mohamedar97" + ] + }, + { + "pr": "14311", + "title": "[FIX] Remove reference to inexistent field when deleting message in thread", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.1.0-rc.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14576", + "title": "Fix: Add emoji shortnames to emoji's list", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "1.1.0-rc.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14600", + "title": "Ci improvements", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "14580", + "title": "[FIX] E2E messages not decrypting in message threads", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14593", + "title": "Fix: emoji render performance for alias", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "14568", + "title": "[FIX] Send replyTo for livechat offline messages", + "userLogin": "renatobecker", + "milestone": "1.2.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "14595", + "title": "Federation i18n message changes", + "userLogin": "alansikora", + "contributors": [ + "alansikora" + ] + }, + { + "pr": "14565", + "title": "[REGRESSION] Fix Slack bridge channel owner on channel creation", + "userLogin": "MarcosSpessatto", + "milestone": "1.2.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14581", + "title": "Fix thumbs up emoji shortname", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14572", + "title": "[Fix] broken logo url in app.json", + "userLogin": "jaredmoody", + "contributors": [ + "jaredmoody" + ] + }, + { + "pr": "14583", + "title": "Add digitalocean button to readme", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow" + ] + } + ] + }, + "1.1.0-rc.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14614", + "title": "[FIX] Mailer breaking if user doesn't have an email address", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.0", + "contributors": [ + "MarcosSpessatto" + ] + } + ] + }, + "1.1.0-rc.4": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14625", + "title": "[FIX] Role name spacing on Permissions page", + "userLogin": "tassoevan", + "milestone": "1.1.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14590", + "title": "[FIX] Avatar images on old Livechat client", + "userLogin": "arminfelder", + "contributors": [ + "arminfelder", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14623", + "title": "[FIX] Inject code at the end of tag", + "userLogin": "tassoevan", + "milestone": "1.1.0", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.1.0-rc.5": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14646", + "title": "Improvement: Permissions table", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14641", + "title": "Regression: Handle missing emojis", + "userLogin": "sampaiodiego", + "milestone": "1.1.0", + "contributors": [ + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14651", + "title": "[FIX] \"Blank page\" on safari 10.x", + "userLogin": "ggazzo", + "milestone": "1.1.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14643", + "title": "LingoHub based on develop", + "userLogin": "engelgabriel", + "contributors": [ + "sampaiodiego" + ] + } + ] + }, + "1.1.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14446", + "title": "Release 1.0.3", + "userLogin": "rodrigok", + "milestone": "1.0.3", + "contributors": [ + "rodrigok", + "mrsimpson", + "ggazzo", + "MarcosSpessatto", + "geekgonecrazy", + "tassoevan", + "engelgabriel", + "sampaiodiego" + ] + }, + { + "pr": "14339", + "title": "Release 1.0.2", + "userLogin": "rodrigok", + "contributors": [ + "tassoevan", + "rodrigok", + "mohamedar97", + "ggazzo", + "thaiphv", + "knrt10", + "lolimay", + "AnBo83", + "sampaiodiego" + ] + }, + { + "pr": "14338", + "title": "[FIX] Unread line and new day separator were not aligned ", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14336", + "title": "[FIX] Audio notification for messages on DM", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14334", + "title": "[IMPROVE] i18n of threads and discussion buttons", + "userLogin": "sampaiodiego", + "milestone": "1.0.2", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14330", + "title": "[FIX] Duplicate thread message after editing", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14328", + "title": "[FIX] New day separator rendered over thread reply", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14320", + "title": "[IMPROVE] Better error message when not able to get MongoDB Version", + "userLogin": "rodrigok", + "milestone": "1.0.2", + "contributors": [ + "rodrigok", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14318", + "title": "Add cross-browser select arrow positioning", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14182", + "title": "i18n: Update German strings", + "userLogin": "AnBo83", + "milestone": "1.0.2", + "contributors": [ + "AnBo83", + "web-flow" + ] + }, + { + "pr": "14011", + "title": "[FIX] Missing i18n for some new Permissions", + "userLogin": "lolimay", + "milestone": "1.0.2", + "contributors": [ + "lolimay", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14316", + "title": "[FIX] View Logs admin page was broken and not rendering color logs", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan", + "web-flow" + ] + }, + { + "pr": "14301", + "title": "[Regression] Anonymous user fix", + "userLogin": "knrt10", + "milestone": "1.0.2", + "contributors": [ + "knrt10" + ] + }, + { + "pr": "14299", + "title": "Coerces the MongoDB version string", + "userLogin": "thaiphv", + "milestone": "1.0.2", + "contributors": [ + "thaiphv" + ] + }, + { + "pr": "14313", + "title": "[FIX] show roles on message", + "userLogin": "ggazzo", + "milestone": "1.0.2", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14075", + "title": "[Fix] group name appears instead of the room id", + "userLogin": "mohamedar97", + "milestone": "1.0.2", + "contributors": [ + "mohamedar97" + ] + }, + { + "pr": "14311", + "title": "[FIX] Remove reference to inexistent field when deleting message in thread", + "userLogin": "tassoevan", + "milestone": "1.0.2", + "contributors": [ + "tassoevan" + ] + } + ] + }, + "1.1.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14686", + "title": "[FIX] SAML login error.", + "userLogin": "Hudell", + "milestone": "1.1.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14668", + "title": "[FIX] Load messages after disconnect and message box scroll missing", + "userLogin": "ggazzo", + "milestone": "1.1.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14674", + "title": "Removing unnecesary federation configs", + "userLogin": "alansikora", + "milestone": "1.1.1", + "contributors": [ + "alansikora" + ] + } + ] + }, + "1.1.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14823", + "title": "Release 1.1.2", + "userLogin": "sampaiodiego", + "contributors": [ + "Hudell", + "sampaiodiego", + "MarcosSpessatto" + ] + }, + { + "pr": "14763", + "title": "[FIX] User status information on User Info panel", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.2", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14711", + "title": "[FIX] User Real Name being erased when not modified", + "userLogin": "Hudell", + "milestone": "1.1.2", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14717", + "title": "[FIX] Anonymous chat read", + "userLogin": "sampaiodiego", + "milestone": "1.1.2", + "contributors": [ + "sampaiodiego", + "ggazzo", + "MarcosSpessatto", + "web-flow" + ] + } + ] + }, + "1.1.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14850", + "title": "Release 1.1.3", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "14839", + "title": "Regression: thread loading parent msg if is not loaded", + "userLogin": "ggazzo", + "milestone": "1.1.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14837", + "title": "[FIX] Gap of messages when loading history when using threads", + "userLogin": "ggazzo", + "milestone": "1.1.3", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "1.2.0-rc.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "13933", + "title": "[NEW] Custom User Status", + "userLogin": "Hudell", + "milestone": "1.2.0", + "contributors": [ + "Hudell", + "web-flow", + "engelgabriel", + "wreiske" + ] + }, + { + "pr": "14645", + "title": "[FIX] Error when using Download My Data or Export My Data", + "userLogin": "Hudell", + "contributors": [ + "Hudell", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14175", + "title": "[IMPROVE] Adds link to download generated user data file", + "userLogin": "Hudell", + "contributors": [ + "Hudell", + "web-flow", + "MarcosSpessatto" + ] + }, + { + "pr": "14810", + "title": "[FIX] Removes E2E action button, icon and banner when E2E is disabled.", + "userLogin": "marceloschmidt", + "milestone": "1.2.0", + "contributors": [ + "marceloschmidt" + ] + }, + { + "pr": "13900", + "title": "[IMPROVE] Layout of livechat manager pages to new style", + "userLogin": "ggazzo", + "milestone": "1.2.0", + "contributors": [ + "renatobecker", + "engelgabriel", + "web-flow", + "ggazzo" + ] + }, + { + "pr": "14839", + "title": "Regression: thread loading parent msg if is not loaded", + "userLogin": "ggazzo", + "milestone": "1.1.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14837", + "title": "[FIX] Gap of messages when loading history when using threads", + "userLogin": "ggazzo", + "milestone": "1.1.3", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14838", + "title": "[NEW] changed mongo version for snap from 3.2.7 to 3.4.20", + "userLogin": "LuluGO", + "contributors": [ + "LuluGO" + ] + }, + { + "pr": "14759", + "title": "[NEW] Add loading animation to webdav file picker", + "userLogin": "ubarsaiyan", + "contributors": [ + "ubarsaiyan", + "web-flow" + ] + }, + { + "pr": "14717", + "title": "[FIX] Anonymous chat read", + "userLogin": "sampaiodiego", + "milestone": "1.1.2", + "contributors": [ + "sampaiodiego", + "ggazzo", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14699", + "title": "[NEW] Add tmid property to outgoing integration", + "userLogin": "MarcosSpessatto", + "milestone": "1.2.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14805", + "title": "Fix not fully extracted pieces", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14710", + "title": "[FIX] Assume microphone is available", + "userLogin": "tassoevan", + "milestone": "1.2.0", + "contributors": [ + "tassoevan", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14714", + "title": "[NEW] Endpoint to anonymously read channel's messages", + "userLogin": "PrajvalRaval", + "milestone": "1.2.0", + "contributors": [ + "PrajvalRaval", + "web-flow", + "MarcosSpessatto" + ] + }, + { + "pr": "14665", + "title": "[FIX] Move the set Avatar call on user creation to make sure the user has username", + "userLogin": "MarcosSpessatto", + "milestone": "1.2.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14763", + "title": "[FIX] User status information on User Info panel", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.2", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14804", + "title": "Regression: Fix file upload", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14777", + "title": "Extract permissions functions", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14779", + "title": "[NEW] Add Livechat inquiries endpoints", + "userLogin": "MarcosSpessatto", + "milestone": "1.2.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14754", + "title": "Add custom fileupload whitelist property", + "userLogin": "renatobecker", + "milestone": "1.2.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "14724", + "title": "[FIX] users typing forever", + "userLogin": "ggazzo", + "milestone": "1.2.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14709", + "title": "[FIX] Increasing time to rate limit in shield.svg endpoint and add a setting to disable API rate limiter", + "userLogin": "MarcosSpessatto", + "milestone": "1.2.0", + "contributors": [ + "MarcosSpessatto", + "web-flow", + "rodrigok" + ] + }, + { + "pr": "14711", + "title": "[FIX] User Real Name being erased when not modified", + "userLogin": "Hudell", + "milestone": "1.1.2", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14690", + "title": "[NEW] Configuration to limit amount of livechat inquiries displayed", + "userLogin": "renatobecker", + "milestone": "1.2.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14569", + "title": "[FIX] Wrong filter field when filtering current Livechats", + "userLogin": "renatobecker", + "milestone": "1.2.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "14589", + "title": "[IMPROVE] Add an optional rocketchat-protocol DNS entry for Federation", + "userLogin": "alansikora", + "milestone": "1.2.0", + "contributors": [ + "alansikora" + ] + }, + { + "pr": "14471", + "title": "[FIX] Import Chart.js error", + "userLogin": "sonbn0", + "contributors": [ + "sonbn0", + "web-flow", + "Hudell" + ] + }, + { + "pr": "14533", + "title": "[FIX] Name is undefined in some emails", + "userLogin": "MarcosSpessatto", + "milestone": "1.2.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14624", + "title": "[IMPROVE] Use configurable colors on sidebar items", + "userLogin": "tassoevan", + "milestone": "1.2.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14686", + "title": "[FIX] SAML login error.", + "userLogin": "Hudell", + "milestone": "1.1.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14674", + "title": "Removing unnecesary federation configs", + "userLogin": "alansikora", + "milestone": "1.1.1", + "contributors": [ + "alansikora" + ] + }, + { + "pr": "14656", + "title": "Merge master into develop & Set version to 1.2.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "tassoevan", + "rodrigok", + "mohamedar97", + "ggazzo", + "thaiphv", + "knrt10", + "lolimay", + "AnBo83", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14668", + "title": "[FIX] Load messages after disconnect and message box scroll missing", + "userLogin": "ggazzo", + "milestone": "1.1.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14493", + "title": "[FIX] Direct reply delete config and description", + "userLogin": "ruKurz", + "contributors": [ + "ruKurz", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14850", + "title": "Release 1.1.3", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "14763", + "title": "[FIX] User status information on User Info panel", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.2", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14711", + "title": "[FIX] User Real Name being erased when not modified", + "userLogin": "Hudell", + "milestone": "1.1.2", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14717", + "title": "[FIX] Anonymous chat read", + "userLogin": "sampaiodiego", + "milestone": "1.1.2", + "contributors": [ + "sampaiodiego", + "ggazzo", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14686", + "title": "[FIX] SAML login error.", + "userLogin": "Hudell", + "milestone": "1.1.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14668", + "title": "[FIX] Load messages after disconnect and message box scroll missing", + "userLogin": "ggazzo", + "milestone": "1.1.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14674", + "title": "Removing unnecesary federation configs", + "userLogin": "alansikora", + "milestone": "1.1.1", + "contributors": [ + "alansikora" + ] + } + ] + }, + "1.2.0-rc.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14853", + "title": "[FIX] Custom status fixes", + "userLogin": "wreiske", + "milestone": "1.2.0-rc.1", + "contributors": [ + "wreiske", + "Hudell", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14860", + "title": "Regression: Fix desktop notifications not being sent", + "userLogin": "sampaiodiego", + "milestone": "1.2.0-rc.1", + "contributors": [ + "sampaiodiego" + ] + }, + { + "pr": "14862", + "title": "Regression: Allow debugging of cached collections by name", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14859", + "title": "Allow debugging of cached collections by name", + "userLogin": "ggazzo", + "milestone": "1.2.0-rc.1", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + } + ] + }, + "1.2.0-rc.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14886", + "title": "[NEW] Show App bundles and its apps", + "userLogin": "rodrigok", + "milestone": "1.2.0", + "contributors": [ + "rodrigok", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14887", + "title": "[FIX] LinkedIn OAuth login", + "userLogin": "engelgabriel", + "milestone": "1.2.0", + "contributors": [ + "Hudell" + ] + } + ] + }, + "1.2.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14850", + "title": "Release 1.1.3", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego" + ] + }, + { + "pr": "14763", + "title": "[FIX] User status information on User Info panel", + "userLogin": "MarcosSpessatto", + "milestone": "1.1.2", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14711", + "title": "[FIX] User Real Name being erased when not modified", + "userLogin": "Hudell", + "milestone": "1.1.2", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14717", + "title": "[FIX] Anonymous chat read", + "userLogin": "sampaiodiego", + "milestone": "1.1.2", + "contributors": [ + "sampaiodiego", + "ggazzo", + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14686", + "title": "[FIX] SAML login error.", + "userLogin": "Hudell", + "milestone": "1.1.1", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14668", + "title": "[FIX] Load messages after disconnect and message box scroll missing", + "userLogin": "ggazzo", + "milestone": "1.1.1", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14674", + "title": "Removing unnecesary federation configs", + "userLogin": "alansikora", + "milestone": "1.1.1", + "contributors": [ + "alansikora" + ] + } + ] + }, + "1.3.0-rc.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14954", + "title": "[NEW] Show helpful error when oplog is missing", + "userLogin": "justinr1234", + "contributors": [ + "justinr1234", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14948", + "title": "[NEW] Subscription enabled marketplace", + "userLogin": "d-gubert", + "contributors": [ + "graywolf336", + "d-gubert", + "web-flow", + "tassoevan" + ] + }, + { + "pr": "15025", + "title": "[NEW] Deprecate MongoDB version 3.2", + "userLogin": "rodrigok", + "milestone": "1.3.0", + "contributors": [ + "rodrigok" + ] + }, + { + "pr": "14622", + "title": "[FIX] Russian grammatical errors", + "userLogin": "BehindLoader", + "contributors": [ + "BehindLoader" + ] + }, + { + "pr": "14412", + "title": "[FIX] Message attachments not allowing float numbers", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14515", + "title": "Wrong text when reporting a message", + "userLogin": "zdumitru", + "contributors": [ + "zdumitru", + "web-flow" + ] + }, + { + "pr": "14833", + "title": "[FIX] Typo in german translation", + "userLogin": "Le-onardo", + "contributors": [ + null, + "Le-onardo" + ] + }, + { + "pr": "15019", + "title": "[NEW] Options to filter discussion and livechat on Admin > Rooms", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14916", + "title": "[FIX] users.setStatus REST endpoint not allowing reset status message", + "userLogin": "cardoso", + "contributors": [ + "cardoso" + ] + }, + { + "pr": "15013", + "title": "Add missing French translation", + "userLogin": "commiaI", + "contributors": [ + "commiaI", + "web-flow" + ] + }, + { + "pr": "15014", + "title": "[NEW] Settings to further customize GitLab OAuth", + "userLogin": "Hudell", + "milestone": "1.3.0", + "contributors": [ + "Hudell", + "web-flow" + ] + }, + { + "pr": "14935", + "title": "[NEW] Accept multiple redirect URIs on OAuth Apps", + "userLogin": "Hudell", + "milestone": "1.3.0", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14675", + "title": "[NEW] Setting to configure custom authn context on SAML requests", + "userLogin": "Hudell", + "milestone": "1.3.0", + "contributors": [ + "Hudell", + "web-flow" + ] + }, + { + "pr": "15026", + "title": "Fix statistics error for apps on first load", + "userLogin": "d-gubert", + "milestone": "1.3.0", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "15006", + "title": "[FIX] SVG uploads crashing process", + "userLogin": "snoopotic", + "contributors": [ + "snoopotic", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14941", + "title": "Always convert the sha256 password to lowercase on checking", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto", + "sampaiodiego" + ] + }, + { + "pr": "15022", + "title": "[IMPROVE] Connectivity Services License Sync", + "userLogin": "geekgonecrazy", + "contributors": [ + "geekgonecrazy", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "15021", + "title": "[FIX] Edit message with arrow up key if not last message", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14845", + "title": "[FIX] Livechat dashboard average and reaction time labels", + "userLogin": "anandpathak", + "milestone": "1.3.0", + "contributors": [ + "anandpathak" + ] + }, + { + "pr": "14878", + "title": "New: Apps and integrations statistics", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto", + "d-gubert", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14950", + "title": "[FIX] Edit permissions screen", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo", + "MarcosSpessatto", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "15020", + "title": "[FIX] Invite users auto complete cropping results", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14879", + "title": "[NEW] Webdav File Picker", + "userLogin": "ubarsaiyan", + "milestone": "1.3.0", + "contributors": [ + "ubarsaiyan", + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14884", + "title": "improve: relocate some of wizard info to register", + "userLogin": "geekgonecrazy", + "milestone": "1.3.0", + "contributors": [ + "geekgonecrazy", + "rodrigok", + "web-flow" + ] + }, + { + "pr": "14861", + "title": "[FIX] Always displaying jumbomojis when using \"marked\" markdown", + "userLogin": "brakhane", + "milestone": "1.3.0", + "contributors": [ + "brakhane", + "tassoevan", + "web-flow" + ] + }, + { + "pr": "15004", + "title": "[IMPROVE] Add flag to identify remote federation users", + "userLogin": "alansikora", + "contributors": [ + "alansikora", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "15000", + "title": "[FIX] CustomOauth Identity Step errors displayed in HTML format", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "15001", + "title": "[FIX] Custom User Status throttled by rate limiter", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14468", + "title": "[FIX] Not being able to mention users with \"all\" and \"here\" usernames - do not allow users register that usernames", + "userLogin": "hamidrezabstn", + "contributors": [ + "hamidrezabstn", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14457", + "title": "Improve Docker compose readability", + "userLogin": "NateScarlet", + "contributors": [ + "NateScarlet", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14992", + "title": "[IMPROVE] Extract federation config to its own file", + "userLogin": "d-gubert", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "14969", + "title": "Bump marked from 0.5.2 to 0.6.1", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow", + "engelgabriel", + "sampaiodiego" + ] + }, + { + "pr": "14971", + "title": "Remove unused Meteor dependency (yasinuslu:blaze-meta)", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "14977", + "title": "Bump photoswipe version to 4.1.3", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14976", + "title": "Bump node-rsa version to 1.0.5", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14974", + "title": "Bump juice version to 5.2.0", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14973", + "title": "Remove unused dependency (lokijs)", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan", + "engelgabriel", + "web-flow" + ] + }, + { + "pr": "14966", + "title": "[FIX] Users staying online after logout", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14980", + "title": "Regression: patch to improve emoji render", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14722", + "title": "[IMPROVEMENT] patch to improve emoji render", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14965", + "title": "[FIX] Chrome doesn't load additional search results when bottom is reached", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14960", + "title": "[FIX] Wrong label order on room settings", + "userLogin": "Hudell", + "contributors": [ + "Hudell" + ] + }, + { + "pr": "14970", + "title": "[FIX] Allow storing the navigation history of unregistered Livechat visitors", + "userLogin": "renatobecker", + "milestone": "1.3.0", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "14922", + "title": "Bump jquery from 3.3.1 to 3.4.0 in /packages/rocketchat-livechat/.app", + "userLogin": "dependabot[bot]", + "contributors": [ + "dependabot[bot]", + "web-flow" + ] + }, + { + "pr": "14951", + "title": "[FIX] 50 custom emoji limit", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14952", + "title": "[FIX] eternal loading file list", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14967", + "title": "[FIX] load more messages", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14968", + "title": "[FIX] Loading indicator positioning", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14964", + "title": "[IMPROVE] Update tabs markup", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14949", + "title": "[FIX] Jump to message missing in Starred Messages", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14963", + "title": "[IMPROVE] Remove too specific helpers isFirefox() and isChrome()", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "14753", + "title": "[FIX] Method `getUsersOfRoom` not returning offline users if limit is not defined", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14953", + "title": "[FIX] OTR key icon missing on messages", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14945", + "title": "[FIX] Prevent error on trying insert message with duplicated id", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto", + "web-flow" + ] + }, + { + "pr": "14808", + "title": "[FIX] LDAP login with customField sync", + "userLogin": "magicbelette", + "milestone": "1.3.0", + "contributors": [ + "magicbelette", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "14958", + "title": "[FIX]Wrong custom status displayed on room leader panel", + "userLogin": "Hudell", + "contributors": [ + "Hudell", + "sampaiodiego", + "web-flow" + ] + }, + { + "pr": "14921", + "title": "[NEW] Setting to prevent Livechat agents online when Office Hours are closed", + "userLogin": "renatobecker", + "milestone": "1.3.0", + "contributors": [ + "renatobecker", + "web-flow" + ] + }, + { + "pr": "14915", + "title": "Callbacks perf", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow", + "sampaiodiego", + "tassoevan" + ] + }, + { + "pr": "14917", + "title": "Split oplog emitters in files", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "14909", + "title": "Extract canSendMessage function", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo", + "web-flow" + ] + }, + { + "pr": "14671", + "title": "[FIX] Video recorder message echo", + "userLogin": "vova-zush", + "milestone": "1.2.0", + "contributors": [ + "vova-zush" + ] + }, + { + "pr": "14785", + "title": "[FIX] Opening Livechat messages on mobile apps", + "userLogin": "zolbayars", + "milestone": "1.3.0", + "contributors": [ + "zolbayars", + "web-flow" + ] + }, + { + "pr": "14852", + "title": "[IMPROVE] Add descriptions on user data download buttons and popup info", + "userLogin": "MarcosSpessatto", + "milestone": "1.3.0", + "contributors": [ + "MarcosSpessatto" + ] + }, + { + "pr": "14880", + "title": "[FIX] SAML login by giving displayName priority over userName for fullName", + "userLogin": "pkolmann", + "milestone": "1.3.0", + "contributors": [ + "pkolmann" + ] + }, + { + "pr": "14851", + "title": "Improve: Get public key for marketplace", + "userLogin": "geekgonecrazy", + "milestone": "1.3.0", + "contributors": [ + "geekgonecrazy", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "14894", + "title": "[FIX] Not showing local app on App Details", + "userLogin": "d-gubert", + "milestone": "1.2.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "14889", + "title": "Merge master into develop & Set version to 1.3.0-develop", + "userLogin": "sampaiodiego", + "contributors": [ + "ggazzo", + "sampaiodiego", + "alansikora", + "Hudell", + "MarcosSpessatto", + "web-flow" + ] + } + ] + }, + "1.2.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14898", + "title": "Release 1.2.1", + "userLogin": "sampaiodiego", + "contributors": [ + "d-gubert", + "sampaiodiego" + ] + }, + { + "pr": "14894", + "title": "[FIX] Not showing local app on App Details", + "userLogin": "d-gubert", + "milestone": "1.2.1", + "contributors": [ + "d-gubert" + ] + } + ] + }, + "1.3.0-rc.1": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15041", + "title": "Regression: fix code style, setup wizard error and profile page header", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "15039", + "title": "Regression: Framework version being attached to a request that doesn't require it", + "userLogin": "graywolf336", + "contributors": [ + "graywolf336" + ] + } + ] + }, + "1.3.0-rc.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15046", + "title": "Update Livechat widget", + "userLogin": "renatobecker", + "milestone": "1.3.0", + "contributors": [ + "renatobecker" + ] + } + ] + }, + "1.0.4": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15054", + "title": "[FIX] Not sanitized message types", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "1.1.4": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15054", + "title": "[FIX] Not sanitized message types", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "1.2.2": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15054", + "title": "[FIX] Not sanitized message types", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "1.2.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [] + }, + "1.3.0-rc.3": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15067", + "title": "Regression: getSetupWizardParameters", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "15060", + "title": "[FIX] setupWizard calling multiple getSetupWizardParameters", + "userLogin": "ggazzo", + "milestone": "1.3.0", + "contributors": [ + "ggazzo" + ] + }, + { + "pr": "15027", + "title": "Regression: Webdav File Picker search and fixed overflows", + "userLogin": "ubarsaiyan", + "milestone": "1.3.0", + "contributors": [ + "ubarsaiyan", + "ggazzo" + ] + }, + { + "pr": "15054", + "title": "[FIX] Not sanitized message types", + "userLogin": "ggazzo", + "contributors": [ + "ggazzo" + ] + } + ] + }, + "1.3.0-rc.4": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "15080", + "title": "Regression: Improve apps bridges for HA setup", + "userLogin": "d-gubert", + "milestone": "1.3.0", + "contributors": [ + "d-gubert", + "web-flow", + "sampaiodiego" + ] + }, + { + "pr": "15075", + "title": "Regression: displaying errors for apps not installed from Marketplace", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan", + "ggazzo", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "15076", + "title": "Regression: Marketplace app pricing plan description", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan", + "d-gubert", + "web-flow" + ] + }, + { + "pr": "15077", + "title": "Regression: uninstall subscribed app modal", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan" + ] + }, + { + "pr": "15045", + "title": "Regression: Apps and Marketplace UI issues", + "userLogin": "tassoevan", + "milestone": "1.3.0", + "contributors": [ + "tassoevan", + "rodrigok", + "geekgonecrazy", + "graywolf336", + "d-gubert" + ] + } + ] + }, + "1.3.0": { + "node_version": "8.11.4", + "npm_version": "6.4.1", + "mongo_versions": [ + "3.2", + "3.4", + "3.6", + "4.0" + ], + "pull_requests": [ + { + "pr": "14898", + "title": "Release 1.2.1", + "userLogin": "sampaiodiego", + "contributors": [ + "d-gubert", + "sampaiodiego" + ] + }, + { + "pr": "14894", + "title": "[FIX] Not showing local app on App Details", + "userLogin": "d-gubert", + "milestone": "1.2.1", + "contributors": [ + "d-gubert" + ] + } + ] } } } \ No newline at end of file diff --git a/.github/templates/commit.hbs b/.github/templates/commit.hbs deleted file mode 100755 index 1781268b2777..000000000000 --- a/.github/templates/commit.hbs +++ /dev/null @@ -1,40 +0,0 @@ -{{!-- pr reference --}}- {{#if pr}}[#{{pr}}]({{pr_url}}){{/if}} - -{{~!-- subject --}} {{subject}} - -{{~!-- commit references --}} -{{~#if references~}} - , closes - {{~#each references}} {{#if @root.linkReferences~}} - [ - {{~#if this.owner}} - {{~this.owner}}/ - {{~/if}} - {{~this.repository}}#{{this.issue}}]( - {{~#if @root.repository}} - {{~#if @root.host}} - {{~@root.host}}/ - {{~/if}} - {{~#if this.repository}} - {{~#if this.owner}} - {{~this.owner}}/ - {{~/if}} - {{~this.repository}} - {{~else}} - {{~#if @root.owner}} - {{~@root.owner}}/ - {{~/if}} - {{~@root.repository}} - {{~/if}} - {{~else}} - {{~@root.repoUrl}} - {{~/if}}/ - {{~@root.issue}}/{{this.issue}}) - {{~else}} - {{~#if this.owner}} - {{~this.owner}}/ - {{~/if}} - {{~this.repository}}#{{this.issue}} - {{~/if}}{{/each}} -{{~/if}} - diff --git a/.github/templates/footer.hbs b/.github/templates/footer.hbs deleted file mode 100755 index 2aa774f53674..000000000000 --- a/.github/templates/footer.hbs +++ /dev/null @@ -1,11 +0,0 @@ -{{#if noteGroups}} -{{#each noteGroups}} - -### {{title}} - -{{#each notes}} -* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{text}} -{{/each}} -{{/each}} - -{{/if}} diff --git a/.github/templates/header.hbs b/.github/templates/header.hbs deleted file mode 100755 index 313fd6528043..000000000000 --- a/.github/templates/header.hbs +++ /dev/null @@ -1,26 +0,0 @@ - -{{#if isPatch~}} - ## -{{~else~}} - # -{{~/if}} {{#if @root.linkCompare~}} - [{{version}}]( - {{~#if @root.repository~}} - {{~#if @root.host}} - {{~@root.host}}/ - {{~/if}} - {{~#if @root.owner}} - {{~@root.owner}}/ - {{~/if}} - {{~@root.repository}} - {{~else}} - {{~@root.repoUrl}} - {{~/if~}} - /compare/{{previousTag}}...{{currentTag}}) -{{~else}} - {{~version}} -{{~/if}} -{{~#if title}} "{{title}}" -{{~/if}} -{{~#if date}} ({{date}}) -{{/if}} diff --git a/.github/templates/template.hbs b/.github/templates/template.hbs deleted file mode 100755 index 0705b78e3a3e..000000000000 --- a/.github/templates/template.hbs +++ /dev/null @@ -1,22 +0,0 @@ -{{> header}} - -{{#each commitGroups}} - -{{#if collapse}} -
-{{title}} -{{else}} -### {{title}} -{{/if}} - -{{#each commits}} -{{> commit root=@root}} -{{/each}} -{{#if collapse}} -
-{{/if}} - -{{/each}} -{{> footer}} - - diff --git a/.gitignore b/.gitignore index ae7a0245e404..1d778eda57fe 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,6 @@ settings.json build.sh /public/livechat packages/rocketchat-i18n/i18n/livechat.* +tests/end-to-end/temporary_staged_test +.screenshots +/private/livechat diff --git a/.meteor/packages b/.meteor/packages index b1fccebabf03..e6e26370285a 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -3,160 +3,46 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -rocketchat:cors +rocketchat:mongo-config -accounts-facebook@1.3.1 -accounts-github@1.4.1 -accounts-google@1.3.1 -accounts-meteor-developer@1.4.1 -accounts-password@1.5.0 -accounts-twitter@1.4.1 +accounts-facebook@1.3.2 +accounts-github@1.4.2 +accounts-google@1.3.2 +accounts-meteor-developer@1.4.2 +accounts-password@1.5.1 +accounts-twitter@1.4.2 blaze-html-templates -check@1.3.0 +check@1.3.1 ddp-rate-limiter@1.0.7 ddp-common@1.4.0 -dynamic-import@0.3.0 -ecmascript@0.10.6 +dynamic-import@0.5.0 +ecmascript@0.12.4 ejson@1.1.0 email@1.2.3 fastclick@1.0.13 -http@1.4.0 +http@1.4.1 jquery@1.11.10 -logging@1.1.19 -meteor-base@1.3.0 +logging@1.1.20 +meteor-base@1.4.0 mobile-experience@1.0.5 -mongo@1.4.2 +mongo@1.6.0 random@1.1.0 -rate-limit@1.0.8 -reactive-dict@1.2.0 +rate-limit@1.0.9 +reactive-dict@1.2.1 reactive-var@1.0.11 reload@1.2.0 service-configuration@1.0.11 -session@1.1.7 -shell-server@0.3.1 +session@1.2.0 +shell-server@0.4.0 spacebars -standard-minifier-js@2.3.1 -tracker@1.1.3 +standard-minifier-js@2.4.0 +tracker@1.2.0 -rocketchat:2fa -rocketchat:action-links -rocketchat:accounts -rocketchat:analytics -rocketchat:api -rocketchat:assets -rocketchat:authorization -rocketchat:autolinker -rocketchat:autotranslate -rocketchat:bot-helpers -rocketchat:cas -rocketchat:channel-settings -rocketchat:channel-settings-mail-messages -rocketchat:colors -rocketchat:crowd -rocketchat:custom-oauth -rocketchat:custom-sounds -rocketchat:dolphin -rocketchat:drupal -rocketchat:emoji -rocketchat:emoji-custom -rocketchat:emoji-emojione -rocketchat:error-handler -rocketchat:favico -rocketchat:file -rocketchat:file-upload -rocketchat:github-enterprise -rocketchat:gitlab #rocketchat:google-natural-language -rocketchat:google-vision -rocketchat:grant -rocketchat:grant-facebook -rocketchat:grant-github -rocketchat:grant-google -rocketchat:graphql -rocketchat:highlight-words -rocketchat:iframe-login -rocketchat:importer -rocketchat:importer-csv -rocketchat:importer-hipchat -rocketchat:importer-hipchat-enterprise -rocketchat:importer-slack -rocketchat:importer-slack-users -rocketchat:integrations -rocketchat:internal-hubot -rocketchat:irc -rocketchat:issuelinks -rocketchat:katex -rocketchat:ldap -rocketchat:lib rocketchat:livechat -rocketchat:livestream -rocketchat:logger -rocketchat:login-token -rocketchat:mailer -rocketchat:mapview -rocketchat:markdown -rocketchat:mentions -rocketchat:mentions-flextab -rocketchat:message-action -rocketchat:message-attachments -rocketchat:message-mark-as-unread -rocketchat:message-pin -rocketchat:message-snippet -rocketchat:message-star -rocketchat:migrations rocketchat:monitoring -rocketchat:oauth2-server-config -rocketchat:oembed -rocketchat:otr -rocketchat:postcss -rocketchat:push-notifications -rocketchat:reactions -rocketchat:retention-policy -rocketchat:apps -rocketchat:sandstorm -rocketchat:setup-wizard -rocketchat:slackbridge -rocketchat:slashcommands-archive -rocketchat:slashcommands-asciiarts -rocketchat:slashcommands-create -rocketchat:slashcommands-help -rocketchat:slashcommands-hide -rocketchat:slashcommands-invite -rocketchat:slashcommands-invite-all -rocketchat:slashcommands-join -rocketchat:slashcommands-kick -rocketchat:slashcommands-leave -rocketchat:slashcommands-me -rocketchat:slashcommands-msg -rocketchat:slashcommands-mute -rocketchat:slashcommands-open -rocketchat:slashcommands-topic -rocketchat:slashcommands-unarchive -rocketchat:slider -rocketchat:smarsh-connector -rocketchat:spotify -rocketchat:statistics rocketchat:streamer -rocketchat:theme -rocketchat:tokenpass -rocketchat:tooltip -rocketchat:tutum -rocketchat:ui -rocketchat:ui-account -rocketchat:ui-admin -rocketchat:ui-clean-history -rocketchat:ui-flextab -rocketchat:ui-login -rocketchat:ui-master -rocketchat:ui-message -rocketchat:ui-sidenav -rocketchat:ui-vrecord -rocketchat:user-data-download rocketchat:version -rocketchat:videobridge -rocketchat:webrtc -rocketchat:wordpress -rocketchat:nrr konecty:change-case konecty:delayed-task @@ -166,14 +52,12 @@ konecty:user-presence deepwell:bootstrap-datepicker2 dispatch:run-as-user -francocatena:status jalik:ufs jalik:ufs-gridfs jparker:gravatar kadira:blaze-layout kadira:flow-router keepnox:perfect-scrollbar -kenton:accounts-sandstorm mizzao:autocomplete mizzao:timesync mrt:reactive-store @@ -182,22 +66,35 @@ nimble:restivus nooitaf:colors ostrio:cookies pauli:accounts-linkedin -percolate:synced-cron raix:handlebar-helpers rocketchat:push raix:ui-dropped-event -steffo:meteor-accounts-saml todda00:friendly-slugs -yasaricli:slugify -yasinuslu:blaze-meta -rocketchat:e2e -rocketchat:blockstack -rocketchat:version-check +rocketchat:tap-i18n +underscore@1.0.10 +juliancwirko:postcss +littledata:synced-cron -rocketchat:search -chatpal:search -rocketchat:lazy-load -tap:i18n -underscore -rocketchat:bigbluebutton +edgee:slingshot +jalik:ufs-local@0.2.5 +accounts-base +accounts-oauth +autoupdate +babel-compiler +google-oauth +htmljs +less +matb33:collection-hooks +meteorhacks:inject-initial +oauth +oauth2 +raix:eventemitter +routepolicy +sha +swydo:graphql +templating +webapp +webapp-hashing +rocketchat:oauth2-server +rocketchat:i18n diff --git a/.meteor/platforms b/.meteor/platforms index 81ae7012de9e..8a3a35f9f627 100644 --- a/.meteor/platforms +++ b/.meteor/platforms @@ -1,3 +1,2 @@ browser -ios server diff --git a/.meteor/release b/.meteor/release index 8fed0e8ee874..91e05fc15b2f 100644 --- a/.meteor/release +++ b/.meteor/release @@ -1 +1 @@ -METEOR@1.6.1.3 +METEOR@1.8.0.2 diff --git a/.meteor/versions b/.meteor/versions index 9a11dd0ee272..56029683a499 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -1,60 +1,59 @@ -accounts-base@1.4.2 -accounts-facebook@1.3.1 -accounts-github@1.4.1 -accounts-google@1.3.1 -accounts-meteor-developer@1.4.1 -accounts-oauth@1.1.15 +accounts-base@1.4.3 +accounts-facebook@1.3.2 +accounts-github@1.4.2 +accounts-google@1.3.2 +accounts-meteor-developer@1.4.2 +accounts-oauth@1.1.16 accounts-password@1.5.1 -accounts-twitter@1.4.1 +accounts-twitter@1.4.2 aldeed:simple-schema@1.5.4 allow-deny@1.1.0 -autoupdate@1.4.0 -babel-compiler@7.0.9 -babel-runtime@1.2.2 +autoupdate@1.5.0 +babel-compiler@7.2.4 +babel-runtime@1.3.0 base64@1.0.11 -binary-heap@1.0.10 -blaze@2.3.2 +binary-heap@1.0.11 +blaze@2.3.3 blaze-html-templates@1.1.2 blaze-tools@1.0.10 -boilerplate-generator@1.4.0 -caching-compiler@1.1.12 -caching-html-compiler@1.1.2 +boilerplate-generator@1.6.0 +caching-compiler@1.2.1 +caching-html-compiler@1.1.3 callback-hook@1.1.0 cfs:http-methods@0.0.32 -chatpal:search@0.0.1 check@1.3.1 coffeescript@1.0.17 dandv:caret-position@2.1.1 ddp@1.4.0 -ddp-client@2.3.2 +ddp-client@2.3.3 ddp-common@1.4.0 ddp-rate-limiter@1.0.7 -ddp-server@2.1.2 +ddp-server@2.2.0 deepwell:bootstrap-datepicker2@1.3.0 deps@1.0.12 -diff-sequence@1.1.0 +diff-sequence@1.1.1 dispatch:run-as-user@1.1.1 -dynamic-import@0.3.0 -ecmascript@0.10.9 -ecmascript-runtime@0.5.0 -ecmascript-runtime-client@0.6.2 -ecmascript-runtime-server@0.5.0 +dynamic-import@0.5.1 +ecmascript@0.12.4 +ecmascript-runtime@0.7.0 +ecmascript-runtime-client@0.8.0 +ecmascript-runtime-server@0.7.1 edgee:slingshot@0.7.1 ejson@1.1.0 email@1.2.3 -emojione:emojione@2.2.6 -es5-shim@4.7.3 -facebook-oauth@1.4.1 +es5-shim@4.8.0 +facebook-oauth@1.5.0 fastclick@1.0.13 -francocatena:status@1.5.3 +fetch@0.1.0 geojson-utils@1.0.10 -github-oauth@1.2.0 -google-oauth@1.2.5 +github-oauth@1.2.2 +google-oauth@1.2.6 hot-code-push@1.0.4 html-tools@1.0.11 htmljs@1.0.11 -http@1.4.1 +http@1.4.2 id-map@1.1.0 +inter-process-messaging@0.1.0 jalik:ufs@0.7.5 jalik:ufs-gridfs@0.2.1 jalik:ufs-local@0.2.9 @@ -62,38 +61,41 @@ jparker:crypto-core@0.1.0 jparker:crypto-md5@0.1.1 jparker:gravatar@0.5.1 jquery@1.11.11 +juliancwirko:postcss@2.0.3 kadira:blaze-layout@2.3.0 kadira:flow-router@2.12.1 keepnox:perfect-scrollbar@0.6.8 -kenton:accounts-sandstorm@0.7.0 konecty:change-case@2.3.0 konecty:delayed-task@1.0.0 konecty:mongo-counter@0.0.5_3 konecty:multiple-instances-status@1.1.0 -konecty:user-presence@2.2.0 +konecty:user-presence@2.5.0 launch-screen@1.1.1 -less@2.7.12 +less@2.8.0 +littledata:synced-cron@1.5.1 livedata@1.0.18 localstorage@1.2.0 logging@1.1.20 matb33:collection-hooks@0.8.4 mdg:validation-error@0.5.1 -meteor@1.8.6 -meteor-base@1.3.0 -meteor-developer-oauth@1.2.0 +meteor@1.9.2 +meteor-base@1.4.0 +meteor-developer-oauth@1.2.1 meteorhacks:inject-initial@1.0.4 meteorhacks:meteorx@1.4.1 meteorspark:util@0.2.0 -minifier-css@1.3.1 -minifier-js@2.3.5 -minimongo@1.4.4 +minifier-css@1.4.1 +minifier-js@2.4.0 +minimongo@1.4.5 mizzao:autocomplete@0.5.1 mizzao:timesync@0.3.4 mobile-experience@1.0.5 mobile-status-bar@1.0.14 -modules@0.11.8 -modules-runtime@0.9.2 -mongo@1.4.7 +modern-browsers@0.1.3 +modules@0.13.0 +modules-runtime@0.10.3 +mongo@1.6.0 +mongo-decimal@0.1.0 mongo-dev-server@1.1.0 mongo-id@1.0.7 mongo-livedata@1.0.12 @@ -102,183 +104,57 @@ mystor:device-detection@0.2.0 nimble:restivus@0.8.12 nooitaf:colors@1.1.2_1 npm-bcrypt@0.9.3 -npm-mongo@2.2.34 -oauth@1.2.3 -oauth1@1.2.0 -oauth2@1.2.0 +npm-mongo@3.1.1 +oauth@1.2.7 +oauth1@1.2.2 +oauth2@1.2.1 observe-sequence@1.0.16 ordered-dict@1.1.0 -ostrio:cookies@2.2.4 -pauli:accounts-linkedin@2.1.5 -pauli:linkedin-oauth@1.2.0 -percolate:synced-cron@1.3.2 -promise@0.10.2 +ostrio:cookies@2.3.0 +pauli:accounts-linkedin@5.0.0 +pauli:linkedin-oauth@5.0.0 +promise@0.11.2 raix:eventemitter@0.1.3 raix:eventstate@0.0.4 raix:handlebar-helpers@0.2.5 raix:ui-dropped-event@0.0.7 random@1.1.0 rate-limit@1.0.9 -reactive-dict@1.2.0 +reactive-dict@1.2.1 reactive-var@1.0.11 reload@1.2.0 retry@1.1.0 -rocketchat:2fa@0.0.1 -rocketchat:accounts@0.0.1 -rocketchat:action-links@0.0.1 -rocketchat:analytics@0.0.2 -rocketchat:api@0.0.1 -rocketchat:apps@1.0.0 -rocketchat:assets@0.0.1 -rocketchat:authorization@0.0.1 -rocketchat:autolinker@0.0.1 -rocketchat:autotranslate@0.0.1 -rocketchat:bigbluebutton@0.0.1 -rocketchat:blockstack@0.0.1 -rocketchat:bot-helpers@0.0.1 -rocketchat:cas@1.0.0 -rocketchat:channel-settings@0.0.1 -rocketchat:channel-settings-mail-messages@0.0.1 -rocketchat:colors@0.0.1 -rocketchat:cors@0.0.1 -rocketchat:crowd@1.0.0 -rocketchat:custom-oauth@1.0.0 -rocketchat:custom-sounds@1.0.0 -rocketchat:dolphin@0.0.2 -rocketchat:drupal@0.0.1 -rocketchat:e2e@0.0.1 -rocketchat:emoji@1.0.0 -rocketchat:emoji-custom@1.0.0 -rocketchat:emoji-emojione@0.0.1 -rocketchat:error-handler@1.0.0 -rocketchat:favico@0.0.1 -rocketchat:file@0.0.1 -rocketchat:file-upload@0.0.1 -rocketchat:github-enterprise@0.0.1 -rocketchat:gitlab@0.0.1 -rocketchat:google-vision@0.0.1 -rocketchat:grant@0.0.1 -rocketchat:grant-facebook@0.0.1 -rocketchat:grant-github@0.0.1 -rocketchat:grant-google@0.0.1 -rocketchat:graphql@0.0.1 -rocketchat:highlight-words@0.0.1 rocketchat:i18n@0.0.1 -rocketchat:iframe-login@1.0.0 -rocketchat:importer@0.0.1 -rocketchat:importer-csv@1.0.0 -rocketchat:importer-hipchat@0.0.1 -rocketchat:importer-hipchat-enterprise@1.0.0 -rocketchat:importer-slack@0.0.1 -rocketchat:importer-slack-users@1.0.0 -rocketchat:integrations@0.0.1 -rocketchat:internal-hubot@0.0.1 -rocketchat:irc@0.0.1 -rocketchat:issuelinks@0.0.1 -rocketchat:katex@0.0.1 -rocketchat:lazy-load@0.0.1 -rocketchat:ldap@0.0.1 -rocketchat:lib@0.0.1 rocketchat:livechat@0.0.1 -rocketchat:livestream@0.0.5 -rocketchat:logger@0.0.1 -rocketchat:login-token@1.0.0 -rocketchat:mailer@0.0.1 -rocketchat:mapview@0.0.1 -rocketchat:markdown@0.0.2 -rocketchat:mentions@0.0.1 -rocketchat:mentions-flextab@0.0.1 -rocketchat:message-action@0.0.1 -rocketchat:message-attachments@0.0.1 -rocketchat:message-mark-as-unread@0.0.1 -rocketchat:message-pin@0.0.1 -rocketchat:message-snippet@0.0.1 -rocketchat:message-star@0.0.1 -rocketchat:migrations@0.0.1 +rocketchat:mongo-config@0.0.1 rocketchat:monitoring@2.30.2_3 -rocketchat:nrr@1.0.0 -rocketchat:oauth2-server@2.0.0 -rocketchat:oauth2-server-config@1.0.0 -rocketchat:oembed@0.0.1 -rocketchat:otr@0.0.1 -rocketchat:postcss@1.0.0 +rocketchat:oauth2-server@2.1.0 rocketchat:push@3.3.1 -rocketchat:push-notifications@0.0.1 -rocketchat:reactions@0.0.1 -rocketchat:retention-policy@0.0.1 -rocketchat:sandstorm@0.0.1 -rocketchat:search@0.0.1 -rocketchat:setup-wizard@0.0.1 -rocketchat:slackbridge@0.0.1 -rocketchat:slashcommands-archive@0.0.1 -rocketchat:slashcommands-asciiarts@0.0.1 -rocketchat:slashcommands-create@0.0.1 -rocketchat:slashcommands-help@0.0.1 -rocketchat:slashcommands-hide@0.0.1 -rocketchat:slashcommands-invite@0.0.1 -rocketchat:slashcommands-invite-all@0.0.1 -rocketchat:slashcommands-join@0.0.1 -rocketchat:slashcommands-kick@0.0.1 -rocketchat:slashcommands-leave@0.0.1 -rocketchat:slashcommands-me@0.0.1 -rocketchat:slashcommands-msg@0.0.1 -rocketchat:slashcommands-mute@0.0.1 -rocketchat:slashcommands-open@0.0.1 -rocketchat:slashcommands-topic@0.0.1 -rocketchat:slashcommands-unarchive@0.0.1 -rocketchat:slider@0.0.1 -rocketchat:smarsh-connector@0.0.1 -rocketchat:sms@0.0.1 -rocketchat:spotify@0.0.1 -rocketchat:statistics@0.0.1 -rocketchat:streamer@0.6.2 -rocketchat:theme@0.0.1 -rocketchat:tokenpass@0.0.1 -rocketchat:tooltip@0.0.1 -rocketchat:tutum@0.0.1 -rocketchat:ui@0.1.0 -rocketchat:ui-account@0.1.0 -rocketchat:ui-admin@0.1.0 -rocketchat:ui-clean-history@0.0.1 -rocketchat:ui-flextab@0.1.0 -rocketchat:ui-login@0.1.0 -rocketchat:ui-master@0.1.0 -rocketchat:ui-message@0.1.0 -rocketchat:ui-sidenav@0.1.0 -rocketchat:ui-vrecord@0.0.1 -rocketchat:user-data-download@1.0.0 +rocketchat:streamer@1.0.2 +rocketchat:tap-i18n@1.9.1 rocketchat:version@1.0.0 -rocketchat:version-check@0.0.1 -rocketchat:videobridge@0.2.0 -rocketchat:webrtc@0.0.1 -rocketchat:wordpress@0.0.1 -routepolicy@1.0.13 -server-render@0.3.1 +routepolicy@1.1.0 service-configuration@1.0.11 -session@1.1.7 +session@1.2.0 sha@1.0.9 -shell-server@0.3.1 -shim-common@0.1.0 +shell-server@0.4.0 simple:json-routes@2.1.0 -socket-stream-client@0.1.0 +socket-stream-client@0.2.2 spacebars@1.0.15 spacebars-compiler@1.1.3 -srp@1.0.10 -standard-minifier-js@2.3.4 -steffo:meteor-accounts-saml@0.0.1 +srp@1.0.12 +standard-minifier-js@2.4.0 swydo:graphql@0.4.0 -tap:i18n@1.8.2 templating@1.3.2 templating-compiler@1.3.3 templating-runtime@1.3.2 templating-tools@1.1.2 +tmeasday:check-npm-versions@0.3.2 todda00:friendly-slugs@0.6.0 -tracker@1.1.3 +tracker@1.2.0 twitter-oauth@1.2.0 ui@1.0.13 underscore@1.0.10 url@1.2.0 -webapp@1.5.0 +webapp@1.7.2 webapp-hashing@1.0.9 -yasaricli:slugify@0.0.7 -yasinuslu:blaze-meta@0.3.3 diff --git a/.postcssrc b/.postcssrc index f0e4aa4b697e..3816f7d2c9fa 100644 --- a/.postcssrc +++ b/.postcssrc @@ -1,6 +1,6 @@ { "plugins": { - "postcss-import": {}, + "postcss-easy-import": {}, "postcss-custom-properties": { "preserve": true }, @@ -9,18 +9,12 @@ "postcss-nested": {}, "autoprefixer": { "browsers": [ - "ie > 10", - "last 2 Edge versions", - "last 2 Firefox versions", - "last 1 FirefoxAndroid versions", - "last 2 Chrome versions", - "last 1 ChromeAndroid versions", - "Safari > 7", - "last 2 Opera versions", - "last 2 iOS versions", - "last 1 Android version" + "last 2 versions" ] } }, - "excludedPackages": ["deepwell:bootstrap-datepicker2", "smoral:sweetalert"] + "excludedPackages": [ + "deepwell:bootstrap-datepicker2", + "smoral:sweetalert" + ] } diff --git a/.sandstorm/.gitignore b/.sandstorm/.gitignore deleted file mode 100644 index 8000dd9db47c..000000000000 --- a/.sandstorm/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.vagrant diff --git a/.sandstorm/CHANGELOG.md b/.sandstorm/CHANGELOG.md deleted file mode 100644 index 8d14253d53f6..000000000000 --- a/.sandstorm/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -### FIRST Sandstorm VERSION of Rocket.Chat diff --git a/.sandstorm/README.md b/.sandstorm/README.md deleted file mode 100644 index 17e5bc5bc534..000000000000 --- a/.sandstorm/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Publish commands - -``` -cd Rocket.Chat -vagrant-spk vm up && vagrant-spk dev -^C -vagrant-spk pack ../rocketchat.spk && vagrant-spk publish ../rocketchat.spk && vagrant-spk vm halt -``` - -# Reset commands - -``` -vagrant-spk vm halt && vagrant-spk vm destroy -``` diff --git a/.sandstorm/Vagrantfile b/.sandstorm/Vagrantfile deleted file mode 100644 index c7eee5ae79ea..000000000000 --- a/.sandstorm/Vagrantfile +++ /dev/null @@ -1,104 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Guess at a reasonable name for the VM based on the folder vagrant-spk is -# run from. The timestamp is there to avoid conflicts if you have multiple -# folders with the same name. -VM_NAME = File.basename(File.dirname(File.dirname(__FILE__))) + "_sandstorm_#{Time.now.utc.to_i}" - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -# ugly hack to prevent hashicorp's bitrot. See https://github.com/hashicorp/vagrant/issues/9442 -# this setting is required for pre-2.0 vagrant, but causes an error as of 2.0.3, -# remove entirely when confident nobody uses vagrant 1.x for anything. -unless Vagrant::DEFAULT_SERVER_URL.frozen? - Vagrant::DEFAULT_SERVER_URL.replace('https://vagrantcloud.com') -end - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - # Base on the Sandstorm snapshots of the official Debian 9 (stretch) box with vboxsf support. - config.vm.box = "debian/contrib-stretch64" - config.vm.box_version = "9.3.0" - - if Vagrant.has_plugin?("vagrant-vbguest") then - # vagrant-vbguest is a Vagrant plugin that upgrades - # the version of VirtualBox Guest Additions within each - # guest. If you have the vagrant-vbguest plugin, then it - # needs to know how to compile kernel modules, etc., and so - # we give it this hint about operating system type. - config.vm.guest = "debian" - end - - # We forward port 6080, the Sandstorm web port, so that developers can - # visit their sandstorm app from their browser as local.sandstorm.io:6080 - # (aka 127.0.0.1:6080). - config.vm.network :forwarded_port, guest: 6080, host: 6080 - - # Use a shell script to "provision" the box. This installs Sandstorm using - # the bundled installer. - config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/global-setup.sh", keep_color: true - # Then, do stack-specific and app-specific setup. - config.vm.provision "shell", inline: "sudo bash /opt/app/.sandstorm/setup.sh", keep_color: true - - # Shared folders are configured per-provider since vboxsf can't handle >4096 open files, - # NFS requires privilege escalation every time you bring a VM up, - # and 9p is only available on libvirt. - - # Calculate the number of CPUs and the amount of RAM the system has, - # in a platform-dependent way; further logic below. - cpus = nil - total_kB_ram = nil - - host = RbConfig::CONFIG['host_os'] - if host =~ /darwin/ - cpus = `sysctl -n hw.ncpu`.to_i - total_kB_ram = `sysctl -n hw.memsize`.to_i / 1024 - elsif host =~ /linux/ - cpus = `nproc`.to_i - total_kB_ram = `grep MemTotal /proc/meminfo | awk '{print $2}'`.to_i - elsif host =~ /mingw/ - # powershell may not be available on Windows XP and Vista, so wrap this in a rescue block - begin - cpus = `powershell -Command "(Get-WmiObject Win32_Processor -Property NumberOfLogicalProcessors | Select-Object -Property NumberOfLogicalProcessors | Measure-Object NumberOfLogicalProcessors -Sum).Sum"`.to_i - total_kB_ram = `powershell -Command "Get-CimInstance -class cim_physicalmemory | % $_.Capacity}"`.to_i / 1024 - rescue - end - end - # Use the same number of CPUs within Vagrant as the system, with 1 - # as a default. - # - # Use at least 512MB of RAM, and if the system has more than 2GB of - # RAM, use 1/4 of the system RAM. This seems a reasonable compromise - # between having the Vagrant guest operating system not run out of - # RAM entirely (which it basically would if we went much lower than - # 512MB) and also allowing it to use up a healthily large amount of - # RAM so it can run faster on systems that can afford it. - if cpus.nil? or cpus.zero? - cpus = 1 - end - if total_kB_ram.nil? or total_kB_ram < 2048000 - assign_ram_mb = 512 - else - assign_ram_mb = (total_kB_ram / 1024 / 4) - end - # Actually apply these CPU/memory values to the providers. - config.vm.provider :virtualbox do |vb, override| - vb.cpus = cpus - vb.memory = assign_ram_mb - vb.name = VM_NAME - - override.vm.synced_folder "..", "/opt/app" - override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm" - override.vm.synced_folder "..", "/vagrant", disabled: true - end - config.vm.provider :libvirt do |libvirt, override| - libvirt.cpus = cpus - libvirt.memory = assign_ram_mb - libvirt.default_prefix = VM_NAME - - override.vm.synced_folder "..", "/opt/app", type: "9p", accessmode: "passthrough" - override.vm.synced_folder ENV["HOME"] + "/.sandstorm", "/host-dot-sandstorm", type: "9p", accessmode: "passthrough" - override.vm.synced_folder "..", "/vagrant", type: "9p", accessmode: "passthrough", disabled: true - end -end diff --git a/.sandstorm/build.sh b/.sandstorm/build.sh deleted file mode 100755 index c8a155a2a351..000000000000 --- a/.sandstorm/build.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -x -set -euvo pipefail - -# Make meteor bundle -sudo chown vagrant:vagrant /home/vagrant -R -cd /opt/app -meteor npm install capnp -meteor npm install -meteor build --directory /home/vagrant/ - -export NODE_ENV=production -# Use npm and node from the Meteor dev bundle to install the bundle's dependencies. -TOOL_VERSION=$(meteor show --ejson $(<.meteor/release) | grep '^ *"tool":' | - sed -re 's/^.*"(meteor-tool@[^"]*)".*$/\1/g') -TOOLDIR=$(echo $TOOL_VERSION | tr @ /) -PATH=$HOME/.meteor/packages/$TOOLDIR/mt-os.linux.x86_64/dev_bundle/bin:$PATH -cd /home/vagrant/bundle/programs/server -npm install --production - -# Copy our launcher script into the bundle so the grain can start up. -mkdir -p /home/vagrant/bundle/opt/app/.sandstorm/ -cp /opt/app/.sandstorm/launcher.sh /home/vagrant/bundle/opt/app/.sandstorm/ diff --git a/.sandstorm/description.md b/.sandstorm/description.md deleted file mode 100644 index 7001f7c09a4a..000000000000 --- a/.sandstorm/description.md +++ /dev/null @@ -1 +0,0 @@ -The Complete Open Source Chat Solution. Rocket.Chat is a Web Chat Server, developed in JavaScript. It is a great solution for communities and companies wanting to privately host their own chat service or for developers looking forward to build and evolve their own chat platforms. diff --git a/.sandstorm/global-setup.sh b/.sandstorm/global-setup.sh deleted file mode 100755 index af9d391aaac9..000000000000 --- a/.sandstorm/global-setup.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -x -set -euvo pipefail - -echo localhost > /etc/hostname -hostname localhost -# Install curl that is needed below. -apt-get update -apt-get install -y curl -curl https://install.sandstorm.io/ > /host-dot-sandstorm/caches/install.sh -SANDSTORM_CURRENT_VERSION=$(curl -fs "https://install.sandstorm.io/dev?from=0&type=install") -SANDSTORM_PACKAGE="sandstorm-$SANDSTORM_CURRENT_VERSION.tar.xz" -if [[ ! -f /host-dot-sandstorm/caches/$SANDSTORM_PACKAGE ]] ; then - curl --output "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "https://dl.sandstorm.io/$SANDSTORM_PACKAGE" - mv "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE.partial" "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" -fi -bash /host-dot-sandstorm/caches/install.sh -d -e "/host-dot-sandstorm/caches/$SANDSTORM_PACKAGE" -modprobe ip_tables -# Make the vagrant user part of the sandstorm group so that commands like -# `spk dev` work. -usermod -a -G 'sandstorm' 'vagrant' -# Bind to all addresses, so the vagrant port-forward works. -sudo sed --in-place='' --expression='s/^BIND_IP=.*/BIND_IP=0.0.0.0/' /opt/sandstorm/sandstorm.conf -# TODO: update sandstorm installer script to ask about dev accounts, and -# specify a value for this option in the default config? -if ! grep --quiet --no-messages ALLOW_DEV_ACCOUNTS=true /opt/sandstorm/sandstorm.conf ; then - echo "ALLOW_DEV_ACCOUNTS=true" | sudo tee -a /opt/sandstorm/sandstorm.conf - sudo service sandstorm restart -fi -# Enable apt-cacher-ng proxy to make things faster if one appears to be running on the gateway IP -GATEWAY_IP=$(ip route | grep ^default | cut -d ' ' -f 3) -if nc -z "$GATEWAY_IP" 3142 ; then - echo "Acquire::http::Proxy \"http://$GATEWAY_IP:3142\";" > /etc/apt/apt.conf.d/80httpproxy -fi diff --git a/.sandstorm/launcher.sh b/.sandstorm/launcher.sh deleted file mode 100755 index d63b973fdbd2..000000000000 --- a/.sandstorm/launcher.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -x -set -euvo pipefail - -export METEOR_SETTINGS='{"public": {"sandstorm": true}}' -export NODE_ENV=production -export SETTINGS_HIDDEN="Email,Email_Header,Email_Footer,SMTP_Host,SMTP_Port,SMTP_Username,SMTP_Password,From_Email,SMTP_Test_Button,Invitation_Customized,Invitation_Subject,Invitation_HTML,Accounts_Enrollment_Customized,Accounts_Enrollment_Email_Subject,Accounts_Enrollment_Email,Accounts_UserAddedEmail_Customized,Accounts_UserAddedEmailSubject,Accounts_UserAddedEmail,Forgot_Password_Customized,Forgot_Password_Email_Subject,Forgot_Password_Email,Verification_Customized,Verification_Email_Subject,Verification_Email" -exec node /start.js -p 8000 diff --git a/.sandstorm/pgp-keyring b/.sandstorm/pgp-keyring deleted file mode 100644 index ad9fabd672857d763016de2cb5db5a4c4a322507..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8435 zcmbVx1z1#FxbB{Sp}TVsB$RIH5QZ)#q&ozL4hboxkrF{fLO>7+5fD&PT0o?v8$?1H zq&XY?kH62k=f=5B2P8NX79$6;((a+cZ!@R7afth>6UQ^S*#5?EUBrz6v$~A zaqDN+q3_Fj8PcOy?2+_)56tdpGTbD+u2u|7RhImg=oX&ty6x z+Zj`IYHCoM_w{)0+r2HkW_yJEh>hnX(`M-NorHxss3zkAc1uK^8>4JZZ_(EiCv>`u zzx?QZK^|75CXT{4y0Cn&UGM8p@`-1spNQWBaNe*yM+6>hrhcDYg=vVtTn{^xXB?1f za63{VDEm=Cv`{)YXXr3~pH3G4Rr7R-V1tPL*cXZDjS@QUejiF?fm@D#p>F8 zcZ5r>x8~L3_AV26<8+eF=UFt0C(=pr$$!XV824aHj1rAixNS+#1GF{;Gx$FuGsmAP z;RxcKRU%*eS7p}alF24?WGq#YhnsF@DM*YQFXbLg6X_21#z__60nhH4{ zb??~w$i}=nw=1g2jW1!TH0EFR9jKlCv9B5uP;|Ng#1H^d$%eA9ba$|ELMou`z*)lX z_bs>0<*kgJvxS2bx0Q>tR1{PaP71*R*f5}%&kvLqhJlR;XAH)`#l}LwFvxMRv2Zac zpfCskWq?2_g8>|DqE{C=w#+=`!cSh!RK}U9J`bopDl1Ve?UB+m6wNdp;S5 zMWAEU&FYxrw;9Y+VuG-$NH@KE$|g>kF4Qc`b4P=UhNL)>>C6i42l7UQGckG&wC|U~ zg=4a1_}Ax4<4@BFkjPex0i>u`f^zpzaE$~c;N4DKbdYlOlr&YaVdlY1_xMJ8w+@HV z2+!hT$Tg-6d z=%Idz&GW+JII5PB{Sjd4V_Ua!Or)Q-`e$Z-Wv%sx*VD@OaWjJ|a~SoNo7t844e~9g zNxUMSs0W2{Bp?u2N)j#$hIH(5Gq~6sxy^|hM7nR$uJ%l?i&B7Ij;8#v;2MLmf{>2X zJb(VET=g?Ym7DJc*xK$^JeBaXPSfAUP7^$Fb!vFvw<^MuW7v9a!Y-0nT(zoDiHC4Y zS6wL68oMqayR3~fHm8sQ{^JpFsDUPKvmEH7Z(e$=*z`U*vjcXrq?ui@MFUylS2LsN zo1%M-b*IMq#d9jxxUwlVwl?f^*Qb)P&$G>UWc8<2`tvZVsUF3Qz;M01bd^Vddc}tEHuO z8T``ruO$D#gRj7^+=E>Iz5ajo_)CIo?P28%01zwyfbLk^cvt}d>=e|y*nx!r0Q@o^ znUANdCy0YDvHNfN<4bJ$8yEhP-`0@_ZR$WwVCCw5Y2O24Q7d~3YXHE2gP7CqFB{9h zu(OjFnD^Hp24_4QwC*kNlNGG#zKw^%KNu2fZE;f-lyigiu3FpNQ~)t2hzo5U6qP~D z4qlVC?uxp{hMnTUSm9n*Aeh{HDK-kmDTT+zGU_H z%3J+q_xD=cz!~6taAmkC{04x8OT%U0Qt+GLoj6?WkB2U}Rt|set4cr5@ z|9;c|t}XC;uYpetxQ|daXd8D2E9B+=*K&8Ub#SuzrT9JlU(|nD{-5AwU-_*!^a6ll z4*;MP{8zTC^Q(tIDEj`Dd8z||-5!8((nG z1Ax4X>pgb|J9|$gKQFHcQWk7kHb`Z(6*ni+!pRBwOXY!dxACxX_qMU-20Uy9_!HEXI_=N#L1w#7=>p~FsY*qeXd|New#L;qM+-_J!N2tF!X?DT!zTa-HZBn{ zAp!9vz6^5d4tin2;g|&2SlE{q|Fdz?1Q25ZR)94Oas_}ALtw;^i&lUUWP$;{aV}pv z{|pEe2FJiefHmQQ2314=6as@m;V`gXa3LUppdNq|V~{ZM$zqafTOh8uk@1Hl=3p_) zl{b^?^lz~USh|N|<4{miQPW&yWn<^KCMYBqi{-%N=N=aGe)@@xqeee;nvbM3c zvv;`b;pye=<9p99?7_qEh{&kuq~ymbPg2vKroYI|%YRu=_^Rk_MP=2y>YCd3A6r`6 z+B-VCJ`D^G4SyXO9UGsUUszoFw)}l%b$e%bZ~x%u;nDFWF9-npP3sS{|H6wH<_Vjd5r^jm%l5CVZ?wmaB6qn6YG^^))VwlQt*3qP|4Fk&8N|M=0jc0 znY&W`A!@?uQEOb=r?-O)fv*&9;^jV1qp0AWj(4wLH+s%;O=gW>Nojo1-0DW(DQ3-_ z)xE~6o3v}Zr7v3)>atMj20x43i!T6&1++joJR0~p6MT^18+`$o3~XA)+_pK9{K8+B zhHDdAEveKN7(dsCp=#rZ@#1m#C%VW5dPR+(_6G5@to?f0x$~0ZyVJ~R2II!^bQJe= zdPRDA46NRClJ8#t;ZoebKCS7^F&Y_U^a9N+T8Kt_*EaJlwXvNS#%|+v1@kjMWbJ03 zAqE8lR@1wmhw{9Cvz%kKxnlZ2eQ}z`@tNcde+JoN`Vnbs19o}cw(ht5*Aqv(l_%}% z!L4@GWRS(5rb34~+v3{tlBYAasXxP5^7umi{73S99h(yGbkgb%TV|@+A)T8yH%%Sq z@qKQ;9Vw|3nG?VLXyYCQAJUX8Ik;iQboiOG#expckF=75D%)zFE0YTE=@Z`Qh)`nW zS&rS-PJ--Z6Bc(wR8qcy(uQ3xJn}#`A4m6ue?IO?VNQ9fdk7>+rInGtE#Ll@JWnrq zWv~>Toh0P&vfO8kTKc1oM$Lw7S)PPs?po?c!gu49SHtf-%LlAA^YUA#ok~}j78TsCSr z(?T;{ux&JZka7W#tuK{&_2D-Wa4ECYx4np;?doos5FdP>K-jrZRJ}E7l2r%|dENUM z|7Bey?bSO?ePu#LPR`_yEaU2T)8$>P5yv%%S$5d41Z2+NjNW0;cvatH!f^COu41eR zr0>?5qrZCXmpvCcRKcO}px$s5j6Kfu%8w_Uly4-gV>2^GGJJ2D#mEh@MRTCW&qTh9#{rlGbs(HjzJx^yzZ?sHHbY9mbsD(cHEC2$2s2_PgZw z&IOGSjaF& zn^j!^&CFYa2X7kvaVa8N#6}&bgmom})K5;ssXiVJ`E6-svx^ zUB4R9*s$SX20;TIKQ{ChHoNX9#S!V96vPghvL3d)SP2f{8DnvIZ)cHw=u=7+U^U>S zPI1Swfs*g0N?n$nYtDyYrEJ2SN*A&tL%*WOz7gFS?s?uqBZh}@RG~e=3=tCg-HrG> zXrYOcTRI;S#>YM#R-;y1PDa{^7!u1!)%PCIq%k%bmG~%UflryrD;)@-uFjG~9aryv%ddJf>|$HvOktaD%T!P? zN}RU5Np%4{I8ICvT_9JzJ!~GK7NZ^3>Z*)eRP2{hzv!-l%5^?jUK^?RF~i|GWq##I zGJcq<@6bQ-lTVY3Z_(<>H5{@tdsxT)E7+`CKfXOJiKOjDQR1Sp*se#w&7l)PG^gTU zXHajq@4s@fdo^Y{uHbV;){QjaEz+uTw@=M(M(B*+_w`&^izI1_h`ym>P7$@KFzH0! zje5%KU1OCA1P~3nYohX*%8!a9L4AMjUpgk;oti~?uW3;9O4{nCG41*@&L#{o{EIoggxCOnP>2LZ@do8FDldeX8JS7CwXNMUNgoz>PW6xLPsT0VXC{ahf?x! zk;^&p?%QqiVyuv#RG%ZOcwCuqvy8m--1YS-^dUfm0Rg|8i_|ARXH~F$Z?F= zDUcnHvIG{k1R*<=sYN12wWyOfkkhr7Tc$bpvQM@9tCTlg8ah)U+(YDAUm8Eno|rG6 z!wiDP4;JYYrn>eX3`$pFOLJ%WwoLKKFy=KN57l3;8dir3;mhA?NL7n`>Z~&ry55C9 zGMF)6?@b#|`28h&-t&Db0Ls3>c7#oT5Y|xefVVfRmYVVqVt<)+Em%~*> z-(QF?xOP9~d)8KQm46oLI4v_vq4nAB`#t&yGRW$yl6Teo3N8R&VH4mg73_-PqetVJ z?(ojmB9->h@Mp;)Gu%_Dy8gY+u|!&ce#zA>dhhC>MfD)w2v?`5nF_Gm{WalrjiUFD zL{sT``W7woAK~XnvFHP5E3K;PcTZ!GOpyRX)vVaJ7pz0Zl^>$q-7@W=r3+(_<0|qn z?gARs>K+(08aOU!6T5C~I9k(bK>Y~Vrz(qW;5pR009t8=ngiC-)-9n_GMy&9yXJn$ zr?2;5Mx&f{nYbb=#!`vd6G0z+N0OS!8ty)18ewK050-M?l8SfPdOiDaGt#T@Vc96B z?FT;6KA7}tXYCEgT@RBXFGiBX^ku!T+^m^9K6Vz3OYt5Qqc!W;ZGj;tc6S>D2c)>W zW__V6rlgJGT(u@!Gn3w;|eh%NE_&XT#h zWV)Df4VIjxvICo!r@f~~yN?^O&>e}su|Hq%ZfiRK)s#ZfUCwg6Bqkg2ip|sS5HIP- zYR4iH`BrLKh-@=MrCy7#9yYD#(`moc+~a;3##gCtJAZE0cL$uU?ngp5_%X30?Oy~- z5FRY_dwwVzyY-P~^1w~1#GwR>YvUe4Vi#_zhFGYs2EjdGWXZXbSlOxPJ2w8B0vRNp zy(L4J+922!bmv2t+Coy@QzT_&P&>sO`{IjNb>nt;0V&rR*@ef}+_wTZL3y5g;Y{JsL2}fVHo!M~qL=8Xz27ONf^h7J7 z^2xN|?f}ay!DDR&An0Q3?-K<075o3u3Bsxn;l(|fmREyu!gq|RcF}l&SP60KQum7p z#q#-zk~VM2pj2okI86#I=a7-UZH_OluDCS^a3N3M1_BdET|#)r@~knQINlvDVJ@t? z6Fm#5(N`p2Bfsl+f9*3hurSmj(UWU2`??^fp>Z}WH6c!Pq@8~!*I==G!tBT{AlFmM zeWQ3lLzFzFu|@rgEVP^MMc)!vV7GIab)G`5JVo~Y!$kGnJ4%G67B(^#WIZ$5Ekv%# zfdTL&%qltw=K{S-jY1T~Gd7|%rjxN2{s)aOe29B3&v)_)SVHtdH3Iu6=Lo3N541Ht zVe|&fB-i7`VCC}sc%kOp)bM26(Euwzj%3*z>ffXeWn=H!@WI?d+LoPq*qRhDmjbQYN)Ja^ zA-%hw6*d`Km9R_@>#~v@mB$7M@qHqiQO-yBjlB#>_{5qS{r=MP(j3aRL&nA$`lwqB z0fK7$X|j-!H0i8C484q8A@Lv<k!Xf*nR*gS;>WpO|LE^sbgZQ-cXDufRG- z3CjZ!Bi=${o_dNWXT^AKkSVXUq~@pbQseCRnG<_UHQGtN$2Tm6E~*VvDngyvtR6x; z5T((ol1AAb^*XJqEAjN_T1t*|K6j!tN-6@)csXosZ+^h*bv|zm+QnG91`GJ?_o^wE zDSD~#>X%x022oW+3qo#05G^7&LmA+y{p;v53VIbz1)fL}{^yA#-tQC1zfT?uor!>_ zo$4RR4L5##w6f#bs2W&3-5TY*4drF>Tlji&-158qwkU>wGkHb(g}FFeOz)h44!=hP z!>S;!rR3dyX|ik@>JO5lns0>3Zv~`qcf)ft$cDn7?^8gG&?iX_Z#D_iy*!5KtdV~VRqEUoJi zrJPVn*P}71rwJ@+PXsgEwz#D}Y1f^-pLX^8lVVzzi_GLME|X)Vj{S3im}ABP;K8=y zOjjM>#Cgk<8Kheabs|XOF@~0~;WAV6I&T;94KT*;!cz^;_%6nr+im%UXl@|x1Lw}c z*IL9Ayz+Hbs-fpf61cVB*loYkrx+-S3)xhR)*kfvHEFcyI@Nr8(|L=!!z(1mm4ad* ziMBS@{6n9p7S~(mn=TPjXnsB?npmQl;@TC|V-YGodH$Q6*vF2?z3b&a)>Sn0DG}96 zwmf8b*KS$6Pn{1gUO8KSlD8ol0J)=`uK=Lo6Sr`+{I8S*^TGMmANklpzc4lsO zaCByFFfw#`GBI~IF*S2;d3j@ccQ9^ocx!euYIb3FV_|49Fk?1)H)v`xXl`{di2@u1 z009aB1_c6E1>jHs3JDOY{|eXMjIZ4{SQhWpcq@>*A~cKt&7*dvU^OKPAsoC9PNOeQcG zk05~>KShhZ7*vUnqds44vZvVDwqy3W7BEZUg(u|EAT99u~N^76f{nh`9{&iRwHA-Edjt7TTa>d zJL4Q!uKKX6>&qY%b_4trx8vYt(8sqKo7a2{SyjKVX8o|?RMiSDnz5yygs~QzPS2HC z93D^)^pL%T;y{+jjm7P>w$j3m>1&_vr$oeMydbYw(~BlZU%Bf-#Pz`=kO1t6A{$uqfi@vpqK+cUZ>wceNiJkRnvKMX_S2(hYCszBslx{%XZ zuQ%iHF+DL;&tmzrO^qRY1vB;ILLv^35S7xCV;l`DDkl{8NP&`eYMF@xmyv*WU!0~e WWdt-6h8J9bb7{6cta5wCYuC4+1~o?j diff --git a/.sandstorm/rocket.chat-128.svg b/.sandstorm/rocket.chat-128.svg deleted file mode 100644 index 06b1893ee1e7..000000000000 --- a/.sandstorm/rocket.chat-128.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.sandstorm/rocket.chat-150.svg b/.sandstorm/rocket.chat-150.svg deleted file mode 100644 index c1e19551c19a..000000000000 --- a/.sandstorm/rocket.chat-150.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.sandstorm/rocket.chat-24.svg b/.sandstorm/rocket.chat-24.svg deleted file mode 100644 index 31c373726528..000000000000 --- a/.sandstorm/rocket.chat-24.svg +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp deleted file mode 100644 index 7468bf5b82fa..000000000000 --- a/.sandstorm/sandstorm-pkgdef.capnp +++ /dev/null @@ -1,115 +0,0 @@ -@0xbbbe049af795122e; - -using Spk = import "/sandstorm/package.capnp"; -# This imports: -# $SANDSTORM_HOME/latest/usr/include/sandstorm/package.capnp -# Check out that file to see the full, documented package definition format. - -const pkgdef :Spk.PackageDefinition = ( - # The package definition. Note that the spk tool looks specifically for the - # "pkgdef" constant. - - id = "vfnwptfn02ty21w715snyyczw0nqxkv3jvawcah10c6z7hj1hnu0", - # Your app ID is actually its public key. The private key was placed in - # your keyring. All updates must be signed with the same key. - - manifest = ( - # This manifest is included in your app package to tell Sandstorm - # about your app. - - appTitle = (defaultText = "Rocket.Chat"), - - appVersion = 97, # Increment this for every release. - - appMarketingVersion = (defaultText = "0.70.0-develop"), - # Human-readable representation of appVersion. Should match the way you - # identify versions of your app in documentation and marketing. - - actions = [ - # Define your "new document" handlers here. - ( title = (defaultText = "New Rocket.Chat"), - command = .myCommand - # The command to run when starting for the first time. (".myCommand" - # is just a constant defined at the bottom of the file.) - ) - ], - - continueCommand = .myCommand, - # This is the command called to start your app back up after it has been - # shut down for inactivity. Here we're using the same command as for - # starting a new instance, but you could use different commands for each - # case. - - metadata = ( - icons = ( - appGrid = (svg = embed "rocket.chat-128.svg"), - grain = (svg = embed "rocket.chat-24.svg"), - market = (svg = embed "rocket.chat-150.svg"), - ), - - website = "https://rocket.chat", - codeUrl = "https://github.com/RocketChat/Rocket.Chat", - license = (openSource = mit), - categories = [communications, productivity, office, social, developerTools], - - author = ( - contactEmail = "team@rocket.chat", - pgpSignature = embed "pgp-signature", - upstreamAuthor = "Rocket.Chat", - ), - pgpKeyring = embed "pgp-keyring", - - description = (defaultText = embed "description.md"), - shortDescription = (defaultText = "Chat app"), - - screenshots = [ - (width = 1024, height = 696, png = embed "screenshot1.png"), - (width = 1024, height = 696, png = embed "screenshot2.png"), - (width = 1024, height = 696, png = embed "screenshot3.png"), - (width = 1024, height = 696, png = embed "screenshot4.png") - ], - - changeLog = (defaultText = embed "CHANGELOG.md"), - ), - - ), - - sourceMap = ( - # The following directories will be copied into your package. - searchPath = [ - ( sourcePath = "/home/vagrant/bundle" ), - ( sourcePath = "/opt/meteor-spk/meteor-spk.deps" ) - ] - ), - - alwaysInclude = [ "." ], - # This says that we always want to include all files from the source map. - # (An alternative is to automatically detect dependencies by watching what - # the app opens while running in dev mode. To see what that looks like, - # run `spk init` without the -A option.) - - bridgeConfig = ( - viewInfo = ( - eventTypes = [ - (name = "message", verbPhrase = (defaultText = "sent message")), - (name = "privateMessage", verbPhrase = (defaultText = "sent private message"), requiredPermission = (explicitList = void)), - ] - ), - saveIdentityCaps = true, - ), -); - -const myCommand :Spk.Manifest.Command = ( - # Here we define the command used to start up your server. - argv = ["/sandstorm-http-bridge", "8000", "--", "/opt/app/.sandstorm/launcher.sh"], - environ = [ - # Note that this defines the *entire* environment seen by your app. - (key = "PATH", value = "/usr/local/bin:/usr/bin:/bin"), - (key = "SANDSTORM", value = "1"), - (key = "HOME", value = "/var"), - (key = "Statistics_reporting", value = "false"), - (key = "Accounts_AllowUserAvatarChange", value = "false"), - (key = "Accounts_AllowUserProfileChange", value = "false"), - (key = "BABEL_CACHE_DIR", value = "/var/babel_cache") - ] -); diff --git a/.sandstorm/screenshot1.png b/.sandstorm/screenshot1.png deleted file mode 100644 index ec123d99cd035c15cea83571764d0d9f991cf709..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40830 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�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 diff --git a/.sandstorm/screenshot2.png b/.sandstorm/screenshot2.png deleted file mode 100644 index 30713297c2f2ba6c1f58a676ae61cff219f69ccf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60942 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 A$p8QV diff --git a/.sandstorm/screenshot4.png b/.sandstorm/screenshot4.png deleted file mode 100644 index 3955cc694a360a094801cf9794ae265f8006c3c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 219976 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 Vp->We|HfbGO-B)N#UQqx4w?}tANv)l$fQN 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

^Ic-4)M>f1%d3%}u(1qG-QrL~h;{3%w+zgTo7 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 X$;Vh<J02-x#0g?HK^5fQQzzE$2wb-^dBDv23C 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 diff --git a/.sandstorm/setup.sh b/.sandstorm/setup.sh deleted file mode 100755 index 6882afab185d..000000000000 --- a/.sandstorm/setup.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash -set -x -set -euvo pipefail - -apt-get update -apt-get install build-essential git -y - -cd /opt/ - -NODE_ENV=production -PACKAGE=meteor-spk-0.4.1 -PACKAGE_FILENAME="$PACKAGE.tar.xz" -CACHE_TARGET="/host-dot-sandstorm/caches/${PACKAGE_FILENAME}" - -# Fetch meteor-spk tarball if not cached -if [ ! -f "$CACHE_TARGET" ] ; then - curl https://dl.sandstorm.io/${PACKAGE_FILENAME} > "$CACHE_TARGET" -fi - -# Extract to /opt -tar xf "$CACHE_TARGET" - -# Create symlink so we can rely on the path /opt/meteor-spk -ln -s "${PACKAGE}" meteor-spk - -#This will install capnp, the Cap’n Proto command-line tool. -#It will also install libcapnp, libcapnpc, and libkj in /usr/local/lib and headers in /usr/local/include/capnp and /usr/local/include/kj. -curl -O https://capnproto.org/capnproto-c++-0.6.1.tar.gz -tar zxf capnproto-c++-0.6.1.tar.gz -cd capnproto-c++-0.6.1 -./configure -make -j6 check -sudo make install -# inlcude libcapnp and libkj library to dependencies. -cp .libs/* /opt/meteor-spk/meteor-spk.deps/lib/x86_64-linux-gnu/ - -# Add bash, and its dependencies, so they get mapped into the image. -# Bash runs the launcher script. -cp -a /bin/bash /opt/meteor-spk/meteor-spk.deps/bin/ -cp -a /lib/x86_64-linux-gnu/libncurses.so.* /opt/meteor-spk/meteor-spk.deps/lib/x86_64-linux-gnu/ -cp -a /lib/x86_64-linux-gnu/libtinfo.so.* /opt/meteor-spk/meteor-spk.deps/lib/x86_64-linux-gnu/ -# for npm in package.json sharp. -cp -a /lib/x86_64-linux-gnu/libresolv* /opt/meteor-spk/meteor-spk.deps/lib/x86_64-linux-gnu/ - - -# Unfortunately, Meteor does not explicitly make it easy to cache packages, but -# we know experimentally that the package is mostly directly extractable to a -# user's $HOME/.meteor directory. -METEOR_RELEASE=1.6.1.1 -METEOR_PLATFORM=os.linux.x86_64 -METEOR_TARBALL_FILENAME="meteor-bootstrap-${METEOR_PLATFORM}.tar.gz" -METEOR_TARBALL_URL="https://d3sqy0vbqsdhku.cloudfront.net/packages-bootstrap/${METEOR_RELEASE}/${METEOR_TARBALL_FILENAME}" -METEOR_CACHE_TARGET="/host-dot-sandstorm/caches/${METEOR_TARBALL_FILENAME}" - -# Fetch meteor tarball if not cached -if [ ! -f "$METEOR_CACHE_TARGET" ] ; then - curl "$METEOR_TARBALL_URL" > "${METEOR_CACHE_TARGET}.partial" - mv "${METEOR_CACHE_TARGET}"{.partial,} -fi - -# Extract as unprivileged user, which is the usual meteor setup -cd /home/vagrant/ -su -c "tar xf '${METEOR_CACHE_TARGET}'" vagrant -# Link into global PATH -ln -s /home/vagrant/.meteor/meteor /usr/bin/meteor -chown vagrant:vagrant /home/vagrant -R diff --git a/.sandstorm/stack b/.sandstorm/stack deleted file mode 100644 index f148e1141a45..000000000000 --- a/.sandstorm/stack +++ /dev/null @@ -1 +0,0 @@ -meteor diff --git a/.scripts/fix-i18n.js b/.scripts/fix-i18n.js new file mode 100644 index 000000000000..ad8210d7cd33 --- /dev/null +++ b/.scripts/fix-i18n.js @@ -0,0 +1,29 @@ +/** + * This script will: + * + * - remove any duplicated i18n key on the same file; + * - re-order all keys based on source i18n file (en.i18n.json) + * - remove all keys not present in source i18n file + */ + +const fs = require('fs'); + +const fg = require('fast-glob'); + +const fixFiles = (path, source, newlineAtEnd = false) => { + const sourceFile = JSON.parse(fs.readFileSync(`${ path }${ source }`, 'utf8')); + const sourceKeys = Object.keys(sourceFile); + + fg([`${ path }/**/*.i18n.json`]).then((entries) => { + entries.forEach((file) => { + console.log(file); + + const json = JSON.parse(fs.readFileSync(file, 'utf8')); + + fs.writeFileSync(file, `${ JSON.stringify(json, sourceKeys, 2) }${ newlineAtEnd ? '\n' : '' }`); + }); + }); +}; + +fixFiles('./packages/rocketchat-i18n', '/i18n/en.i18n.json'); +fixFiles('./packages/rocketchat-livechat/.app/i18n', '/en.i18n.json'); diff --git a/.scripts/logs.js b/.scripts/logs.js index be65d91454b5..a1acf80b8174 100644 --- a/.scripts/logs.js +++ b/.scripts/logs.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs'); + const semver = require('semver'); const ProgressBar = require('progress'); const _ = require('underscore'); @@ -14,7 +15,7 @@ const historyDataFile = path.join(__dirname, '../.github/history.json'); let historyData = (() => { try { - return require(historyDataFile); + return require(historyDataFile); // eslint-disable-line import/no-dynamic-require } catch (error) { return {}; } diff --git a/.scripts/md.js b/.scripts/md.js index 6f927f192cc2..ac935873a69e 100644 --- a/.scripts/md.js +++ b/.scripts/md.js @@ -1,8 +1,9 @@ const path = require('path'); const fs = require('fs'); +const { execSync } = require('child_process'); + const semver = require('semver'); const _ = require('underscore'); -const { execSync } = require('child_process'); const historyDataFile = path.join(__dirname, '../.github/history.json'); const historyManualDataFile = path.join(__dirname, '../.github/history-manual.json'); @@ -50,7 +51,7 @@ const SummaryNameEmoticons = { const historyData = (() => { try { - return require(historyDataFile); + return require(historyDataFile); // eslint-disable-line import/no-dynamic-require } catch (error) { return {}; } @@ -58,7 +59,7 @@ const historyData = (() => { const historyManualData = (() => { try { - return require(historyManualDataFile); + return require(historyManualDataFile); // eslint-disable-line import/no-dynamic-require } catch (error) { return {}; } diff --git a/.scripts/npm-postinstall.js b/.scripts/npm-postinstall.js new file mode 100644 index 000000000000..434539109341 --- /dev/null +++ b/.scripts/npm-postinstall.js @@ -0,0 +1,13 @@ + +const { execSync } = require('child_process'); + +console.log('Running npm-postinstall.js'); + +execSync('cp node_modules/katex/dist/katex.min.css app/katex/'); + +execSync('mkdir -p public/fonts/'); +execSync('cp node_modules/katex/dist/fonts/* public/fonts/'); + +execSync('cp node_modules/pdfjs-dist/build/pdf.worker.min.js public/'); + +execSync('cd packages/rocketchat-livechat/.app && (meteor npm install || npm install) && cd -'); diff --git a/.scripts/set-version.js b/.scripts/set-version.js index 5a7f89653fc7..6524ce70017b 100644 --- a/.scripts/set-version.js +++ b/.scripts/set-version.js @@ -2,6 +2,7 @@ const path = require('path'); const fs = require('fs'); + const semver = require('semver'); const inquirer = require('inquirer'); // const execSync = require('child_process').execSync; @@ -10,7 +11,7 @@ const git = require('simple-git/promise')(process.cwd()); let pkgJson = {}; try { - pkgJson = require(path.resolve( + pkgJson = require(path.resolve( // eslint-disable-line import/no-dynamic-require process.cwd(), './package.json' )); @@ -20,13 +21,12 @@ try { const files = [ './package.json', - './.sandstorm/sandstorm-pkgdef.capnp', './.travis/snap.sh', './.circleci/snap.sh', './.circleci/update-releases.sh', './.docker/Dockerfile', './.docker/Dockerfile.rhel', - './packages/rocketchat-lib/rocketchat.info', + './packages/rocketchat-utils/rocketchat.info', ]; const readFile = (file) => new Promise((resolve, reject) => { fs.readFile(file, 'utf8', (error, result) => { @@ -84,8 +84,6 @@ git.status() .then((data) => writeFile(file, data.replace(pkgJson.version, version))))); }) .then(() => - // execSync('conventional-changelog --config .github/changelog.js -i HISTORY.md -s'); - inquirer.prompt([{ type: 'confirm', message: 'Commit files?', diff --git a/.scripts/start.js b/.scripts/start.js index 3f08b5608276..5d75151e2256 100644 --- a/.scripts/start.js +++ b/.scripts/start.js @@ -3,22 +3,46 @@ const path = require('path'); const fs = require('fs'); const extend = require('util')._extend; -const { exec } = require('child_process'); +const { spawn } = require('child_process'); +const net = require('net'); + const processes = []; +let exitCode; const baseDir = path.resolve(__dirname, '..'); const srcDir = path.resolve(baseDir); +const isPortTaken = (port) => new Promise((resolve, reject) => { + const tester = net.createServer() + .once('error', (err) => (err.code === 'EADDRINUSE' ? resolve(true) : reject(err))) + .once('listening', () => tester.once('close', () => resolve(false)).close()) + .listen(port); +}); + +const waitPortRelease = (port) => new Promise((resolve, reject) => { + isPortTaken(port).then((taken) => { + if (!taken) { + return resolve(); + } + setTimeout(() => { + waitPortRelease(port).then(resolve).catch(reject); + }, 1000); + }); +}); + const appOptions = { env: { PORT: 3000, ROOT_URL: 'http://localhost:3000', + // MONGO_URL: 'mongodb://localhost:27017/test', + // MONGO_OPLOG_URL: 'mongodb://localhost:27017/local', }, }; function startProcess(opts, callback) { - const proc = exec( + const proc = spawn( opts.command, + opts.params, opts.options ); @@ -43,12 +67,28 @@ function startProcess(opts, callback) { proc.stderr.pipe(logStream); } - proc.on('close', function(code) { - console.log(opts.name, `exited with code ${ code }`); - for (let i = 0; i < processes.length; i += 1) { - processes[i].kill(); + proc.on('exit', function(code, signal) { + if (code != null) { + exitCode = code; + console.log(opts.name, `exited with code ${ code }`); + } else { + console.log(opts.name, `exited with signal ${ signal }`); + } + + processes.splice(processes.indexOf(proc), 1); + + processes.forEach((p) => p.kill()); + + if (processes.length === 0) { + waitPortRelease(appOptions.env.PORT).then(() => { + console.log(`Port ${ appOptions.env.PORT } was released, exiting with code ${ exitCode }`); + process.exit(exitCode); + }).catch((error) => { + console.error(`Error waiting port ${ appOptions.env.PORT } to be released, exiting with code ${ exitCode }`); + console.error(error); + process.exit(exitCode); + }); } - process.exit(code); }); processes.push(proc); } @@ -56,7 +96,10 @@ function startProcess(opts, callback) { function startApp(callback) { startProcess({ name: 'Meteor App', - command: 'node /tmp/build-test/bundle/main.js', + command: 'node', + params: ['/tmp/build-test/bundle/main.js'], + // command: 'node', + // params: ['.meteor/local/build/main.js'], waitForMessage: appOptions.waitForMessage, options: { cwd: srcDir, @@ -68,12 +111,15 @@ function startApp(callback) { function startChimp() { startProcess({ name: 'Chimp', - command: 'npm run chimp-test', + command: 'npm', + params: ['run', 'chimp-test'], + // command: 'exit', + // params: ['2'], options: { env: Object.assign({}, process.env, { - NODE_PATH: `${ process.env.NODE_PATH + - path.delimiter + srcDir + - path.delimiter + srcDir }/node_modules`, + NODE_PATH: `${ process.env.NODE_PATH + + path.delimiter + srcDir + + path.delimiter + srcDir }/node_modules`, }), }, }); diff --git a/.scripts/translationDiff.js b/.scripts/translationDiff.js new file mode 100644 index 000000000000..f3ef783c8ac5 --- /dev/null +++ b/.scripts/translationDiff.js @@ -0,0 +1,38 @@ +#!/usr/bin/env node + +const path = require('path'); +const fs = require('fs'); +const util = require('util'); + +// Convert fs.readFile into Promise version of same +const readFile = util.promisify(fs.readFile); + +const translationDir = path.resolve(__dirname, '../packages/rocketchat-i18n/i18n/'); + +async function translationDiff(source, target) { + console.debug('loading translations from', translationDir); + + function diffKeys(a, b) { + const diff = {}; + Object.keys(a).forEach((key) => { + if (!b[key]) { + diff[key] = a[key]; + } + }); + + return diff; + } + + const sourceTranslations = JSON.parse(await readFile(`${ translationDir }/${ source }.i18n.json`, 'utf8')); + const targetTranslations = JSON.parse(await readFile(`${ translationDir }/${ target }.i18n.json`, 'utf8')); + + return diffKeys(sourceTranslations, targetTranslations); +} + +console.log('Note: You can set the source and target language of the comparison with env-variables SOURCE/TARGET_LANGUAGE'); +const sourceLang = process.env.SOURCE_LANGUAGE || 'en'; +const targetLang = process.env.TARGET_LANGUAGE || 'de'; +translationDiff(sourceLang, targetLang).then((diff) => { + console.log('Diff between', sourceLang, 'and', targetLang); + console.log(JSON.stringify(diff, '', 2)); +}); diff --git a/.scripts/version.js b/.scripts/version.js index cd18c6711439..461003c2d585 100644 --- a/.scripts/version.js +++ b/.scripts/version.js @@ -1,10 +1,9 @@ -/* eslint object-shorthand: 0, prefer-template: 0 */ - const path = require('path'); + let pkgJson = {}; try { - pkgJson = require(path.resolve( + pkgJson = require(path.resolve( // eslint-disable-line import/no-dynamic-require process.cwd(), './package.json' )); diff --git a/.snapcraft/resources/Caddyfile b/.snapcraft/resources/Caddyfile index ce07ab1e2ef9..23299c1d2b7e 100644 --- a/.snapcraft/resources/Caddyfile +++ b/.snapcraft/resources/Caddyfile @@ -1,5 +1,5 @@ -http://:8080 -proxy / localhost:3000 { +_caddy-url_ +proxy / localhost:_port_ { websocket transparent } diff --git a/.snapcraft/resources/initcaddy b/.snapcraft/resources/initcaddy index f52cb80de8a2..b55d20de02b3 100755 --- a/.snapcraft/resources/initcaddy +++ b/.snapcraft/resources/initcaddy @@ -1,3 +1,46 @@ -#!/bin/sh -cp $SNAP/bin/Caddyfile $SNAP_DATA/Caddyfile -echo "Replace $SNAP_DATA/Caddyfile with your own to customize reverse proxy" +#!/bin/bash + +# Config options for Caddyfile +#options="site path port" +options="caddy-url port" + +refresh_opt_in_config() { +# replace an option inside the config file. + opt=$1 + value="$2" + if $(grep -q "_${opt}_" $Caddyfile); then + sed "s,_${opt}_,$value," $Caddyfile 2>/dev/null > ${Caddyfile}.new + mv -f ${Caddyfile}.new $Caddyfile 2>/dev/null + else + echo "Fail to update $opt in Caddyfile" + fi +} + +create_caddyfile(){ +# Copy template to config Caddyfile +cp $SNAP/bin/Caddyfile $SNAP_DATA/Caddyfile +} + +update_caddyfile(){ +# Config file path for Caddyfile +Caddyfile=$SNAP_DATA/Caddyfile + +# Iterate through the config options array +for opt in $options + do + # Use snapctl to get the value registered by the snap set command + refresh_opt_in_config $opt $(snapctl get $opt) +done +} + +caddy="$(snapctl get caddy)" +if [[ $caddy == "disable" ]]; then + echo "Caddy is not enabled, please set caddy-url= and caddy=enable" + exit 1 +fi + +create_caddyfile +update_caddyfile + +echo "Your URL was successfully configured - Please restart rocketchat and caddy services to apply configuration changes" + diff --git a/.snapcraft/resources/prepareRocketChat b/.snapcraft/resources/prepareRocketChat index cd653b6fcfc2..ac352ebda9f7 100755 --- a/.snapcraft/resources/prepareRocketChat +++ b/.snapcraft/resources/prepareRocketChat @@ -25,3 +25,6 @@ fi # sharp needs execution stack removed - https://forum.snapcraft.io/t/snap-and-executable-stacks/1812 ls -l npm/node_modules/sharp/vendor execstack --clear-execstack npm/node_modules/sharp/vendor/lib/librsvg-2.so* + +# Having to manually remove because of latest warning +rm -rf npm/node_modules/meteor/konecty_user-presence/node_modules/colors/lib/.colors.js.swp diff --git a/.snapcraft/resources/preparemongo b/.snapcraft/resources/preparemongo index 332dd7d46847..9cfe098409ec 100755 --- a/.snapcraft/resources/preparemongo +++ b/.snapcraft/resources/preparemongo @@ -2,8 +2,8 @@ if [[ $(uname -m) == "x86_64" ]] then - wget --backups=0 "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz" - tar -zxf ./mongodb-linux-x86_64-ubuntu1604-3.2.7.tgz --strip-components=1 + wget --backups=0 "https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.4.20.tgz" + tar -zxf ./mongodb-linux-x86_64-3.4.20.tgz --strip-components=1 else IFS=" " read -a links <<< $(apt-get -y --print-uris install mongodb | egrep -o "https?://[^']+") for link in ${links[@]} diff --git a/.snapcraft/resources/startRocketChat b/.snapcraft/resources/startRocketChat index 50d257b4515c..b39e22742a73 100755 --- a/.snapcraft/resources/startRocketChat +++ b/.snapcraft/resources/startRocketChat @@ -16,10 +16,20 @@ function start_rocketchat { export NODE_ENV=production export BABEL_CACHE_DIR=/tmp export ROOT_URL=http://localhost - export PORT=3000 - export MONGO_URL=mongodb://localhost:27017/parties - export MONGO_OPLOG_URL=mongodb://localhost:27017/local + export PORT="$(snapctl get port)" + export MONGO_URL="$(snapctl get mongo-url)" + export MONGO_OPLOG_URL="$(snapctl get mongo-oplog-url)" export Accounts_AvatarStorePath=$SNAP_COMMON/uploads + siteurl="$(snapctl get siteurl)" + if [ -n "$siteurl" ]; then + export OVERWRITE_SETTING_Site_Url=$siteurl + fi + + for filename in $SNAP_COMMON/*.env; do + while read env_var; do + export "$env_var" + done < $filename + done node $SNAP/main.js } diff --git a/.snapcraft/snap/hooks/configure b/.snapcraft/snap/hooks/configure new file mode 100755 index 000000000000..3f2a606e7f11 --- /dev/null +++ b/.snapcraft/snap/hooks/configure @@ -0,0 +1,122 @@ +#!/bin/bash + +export PATH="$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH" +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$SNAP/lib:$SNAP/usr/lib:$SNAP/lib/x86_64-linux-gnu:$SNAP/usr/lib/x86_64-linux-gnu" +export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH + +# Obtain caddyurl value +caddyurl="$(snapctl get caddy-url)" +# Validate it +#caddyurl_regex='^https?:\/\/([0-9A-Za-z\.-]+){1,}(\.[a-z\.]{2,6})?([\/\da-z\.-]+)?$' #(supporting path) +caddyurl_regex='^https?:\/\/([0-9A-Za-z\.-]+){1,}(\.[a-z\.]{2,6})?$' +if [ -n "$caddyurl" ]; then + if ! [[ $caddyurl =~ $caddyurl_regex ]]; then + echo "\"$caddyurl\" is not a valid url" >&2 + exit 1 + fi +#else +# if [[ $siteurl =~ ^http: ]] && [[ $siteurl =~ ^http:\/\/([0-9A-Za-z\.-]+){1,}?(\.[a-z\.]{2,6})?\/([\/\da-z\.-]+){1,}$ ]]; then +# path=${siteurl#http://*/} +# site=${siteurl#"http://"} +# site=${site%%/*} +# site=http://$site +# elif [[ $siteurl =~ ^https: ]] && [[ $siteurl =~ ^https:\/\/([0-9A-Za-z\.-]+){1,}?(\.[a-z\.]{2,6})?\/([\/\da-z\.-]+){1,}$ ]]; then +# path=${siteurl#https://*/} +# site=${siteurl#"https://"} +# site=${site%%/*} +# site=https://$site +# else +# path="" +# site=$siteurl +# fi +# snapctl set path=$path +# snapctl set site=$site +fi + +# Obtain caddy value +caddy="$(snapctl get caddy)" +# Validate it +caddy_regex='((enable)|(disable))' +if ! [[ $caddy =~ $caddy_regex ]]; then + echo "\"$caddy_regex\" is not a valid, set to enable or disable" >&2 + exit 1 +else + if [[ $caddy == enable ]]; then + caddyurl="$(snapctl get caddy-url)" + if [ -z "$caddyurl" ]; then + echo "You tried to enable caddy but caddy-url is not set yet, please set up caddy-url first and then enable caddy again" >&2 + snapctl set caddy=disable + exit 1 + else + snapctl set siteurl=$caddyurl + fi + else + siteurl="$(snapctl get siteurl)" + if [ -n "$siteurl" ]; then + snapctl set siteurl= + fi + fi +fi + +# Obtain port value +port="$(snapctl get port)" +# Validate it +port_regex='^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$' +if ! [[ $port =~ $port_regex ]]; then + echo "\"$port\" is not a valid port" >&2 + exit 1 +fi + +# Obtain mongourl value +mongourl="$(snapctl get mongo-url)" +# Validate it +mongourl_regex='^mongodb:\/\/([0-9A-Za-z\.\-\_]+){1,}(([a-z\.\-\_]+){2,6})?(:[0-9]{2,5})?\/([0-9A-Za-z\_\-]+)$' +if ! [[ $mongourl =~ $mongourl_regex ]] ; then + echo "\"$mongourl\" is not a valid url" >&2 + exit 1 +fi + +# Obtain mongooplogurl value +mongooplogurl="$(snapctl get mongo-oplog-url)" +# Validate it +mongooplogurl_regex='^mongodb:\/\/([0-9A-Za-z\.\-\_]+){1,}(([a-z\.\-\_]+){2,6})?(:[0-9]{2,5})?\/local$' +if ! [[ $mongooplogurl =~ $mongooplogurl_regex ]] ; then + echo "\"$mongooplogurl\" is not a valid url" >&2 + exit 1 +fi + +# Obtain site protocol +https="$(snapctl get https)" +# Validate it +https_regex='((enable)|(disable))' +if ! [[ $https =~ $https_regex ]]; then + echo "\"$https\" should be enable or disable" >&2 + exit 1 +else + if [[ $https == enable ]] && [[ $caddyurl =~ ^http: ]]; then + echo "Error: You enabled https but your site URL starts with http, disabling https ..." + snapctl set https=disable + exit 1 + elif [[ $https == enable ]] && [[ $caddyurl =~ ^https: ]]; then + domain=${caddyurl#"https://"} + domain=${domain%%/*} + pubip=$(dig $domain |grep -A1 ";; ANSWER SECTION:" |tail -1 | awk '{print $5}') + if [ -z "$pubip" ]; then + echo "Error: Can't resove DNS query for $domain, check your DNS configuration, disabling https ..." + snapctl set https=disable + exit 1 + else + ip=$(curl ipinfo.io/ip 2>/dev/null) + if [[ $ip != $pubip ]]; then + echo "Error: Your public IP doesn't match the one resolved for caddy-url, disabling https ..." + snapctl set https=disable + exit 1 + fi + fi + elif [[ $https == enable ]] && [ -z "$caddyurl" ]; then + echo "Error: You enabled https but your site URL is empty, please set caddy-url=, disabling https ..." + snapctl set https=disable + exit 1 + fi +fi + diff --git a/.snapcraft/snap/hooks/install b/.snapcraft/snap/hooks/install new file mode 100755 index 000000000000..9a78145595ec --- /dev/null +++ b/.snapcraft/snap/hooks/install @@ -0,0 +1,16 @@ +#!/bin/bash + +# Initialize the CADDY_URL to a default +snapctl set caddy=disable + +# Initialize the PORT to a default +snapctl set port=3000 + +# Initialize the MONGO_URL to a default +snapctl set mongo-url=mongodb://localhost:27017/parties + +# Initialize the MONGO_OPLOG_URL to a default +snapctl set mongo-oplog-url=mongodb://localhost:27017/local + +# Initialize the protocol to a default +snapctl set https=disable diff --git a/.snapcraft/snap/hooks/post-refresh b/.snapcraft/snap/hooks/post-refresh new file mode 100755 index 000000000000..f4172f2dd064 --- /dev/null +++ b/.snapcraft/snap/hooks/post-refresh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Initialize the CADDY_URL to a default +caddy="$(snapctl get caddy)" +if [ -z "$caddy" ]; then + snapctl set caddy=disable +fi + +# Initialize the PORT to a default +port="$(snapctl get port)" +if [ -z "$port" ]; then + snapctl set port=3000 +fi + +# Initialize the MONGO_URL to a default +mongourl="$(snapctl get mongo-url)" +if [ -z "$mongourl" ]; then + snapctl set mongo-url=mongodb://localhost:27017/parties +fi + +# Initialize the MONGO_OPLOG_URL to a default +mongooplogurl="$(snapctl get mongo-oplog-url)" +if [ -z "$mongooplogurl" ]; then + snapctl set mongo-oplog-url=mongodb://localhost:27017/local +fi + +# Initialize the protocol to a default +https="$(snapctl get https)" +if [ -z "$https" ]; then + snapctl set https=disable +fi + diff --git a/.snapcraft/snapcraft.yaml b/.snapcraft/snap/snapcraft.yaml similarity index 90% rename from .snapcraft/snapcraft.yaml rename to .snapcraft/snap/snapcraft.yaml index 50354f7afa7f..ae99be479245 100644 --- a/.snapcraft/snapcraft.yaml +++ b/.snapcraft/snap/snapcraft.yaml @@ -22,7 +22,7 @@ apps: daemon: simple plugs: [network, network-bind] rocketchat-caddy: - command: env LC_ALL=C caddy -conf=$SNAP_DATA/Caddyfile -host=localhost:8080 + command: env LC_ALL=C caddy -conf=$SNAP_DATA/Caddyfile daemon: simple plugs: [network, network-bind] mongo: @@ -36,6 +36,9 @@ apps: plugs: [network] initcaddy: command: env LC_ALL=C initcaddy +hooks: + configure: + plugs: [network] parts: node: plugin: dump @@ -95,3 +98,12 @@ parts: organize: caddy: bin/caddy after: [mongodb] + hooks: + plugin: nil + stage-packages: + - dnsutils + - curl + prime: + - usr + - lib + diff --git a/.stylelintignore b/.stylelintignore index 8e94b289684f..c62f04a77570 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1,4 @@ -packages/rocketchat-theme/client/vendor/fontello/css/fontello.css +app/theme/client/vendor/fontello/css/fontello.css packages/meteor-autocomplete/client/autocomplete.css +app/katex/katex.min.css +app/emoji-emojione/client/*.css diff --git a/.travis.yml b/.travis.yml index b602c51213ae..31f618fadec9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -71,7 +71,6 @@ before_deploy: - source ".travis/setdeploydir.sh" - ".travis/setupsig.sh" - ".travis/namefiles.sh" -- echo ".travis/sandstorm.sh" deploy: - provider: s3 access_key_id: AKIAIKIA7H7D47KUHYCA diff --git a/.travis/sandstorm.sh b/.travis/sandstorm.sh deleted file mode 100755 index 72095e70e1e8..000000000000 --- a/.travis/sandstorm.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -x -set -euvo pipefail -IFS=$'\n\t' - -export SANDSTORM_VERSION=$(curl -f "https://install.sandstorm.io/dev?from=0&type=install") -export PATH=$PATH:/tmp/sandstorm-$SANDSTORM_VERSION/bin - -cd /tmp -curl https://dl.sandstorm.io/sandstorm-$SANDSTORM_VERSION.tar.xz | tar -xJf - - -mkdir -p ~/opt -cd ~/opt -curl https://dl.sandstorm.io/meteor-spk-0.1.8.tar.xz | tar -xJf - -ln -s meteor-spk-0.1.8 meteor-spk -cp -a /bin/bash ~/opt/meteor-spk/meteor-spk.deps/bin/ -cp -a /lib/x86_64-linux-gnu/libncurses.so.* ~/opt/meteor-spk/meteor-spk.deps/lib/x86_64-linux-gnu/ -cp -a /lib/x86_64-linux-gnu/libtinfo.so.* ~/opt/meteor-spk/meteor-spk.deps/lib/x86_64-linux-gnu/ -ln -s $TRAVIS_BUILD_DIR ~/opt/app - -cd /tmp -spk init -p3000 -- nothing -export SANDSTORM_ID="$(grep '\sid =' sandstorm-pkgdef.capnp)" - -cd $TRAVIS_BUILD_DIR -export METEOR_WAREHOUSE_DIR="${METEOR_WAREHOUSE_DIR:-$HOME/.meteor}" -export METEOR_DEV_BUNDLE=$(dirname $(readlink -f "$METEOR_WAREHOUSE_DIR/meteor"))/dev_bundle - -mkdir -p ~/vagrant -tar -zxf /tmp/build/Rocket.Chat.tar.gz --directory ~/vagrant/ -cd ~/vagrant/bundle/programs/server && "$METEOR_DEV_BUNDLE/bin/npm" install -cd $TRAVIS_BUILD_DIR/.sandstorm -sed -i "s/\sid = .*/$SANDSTORM_ID/" sandstorm-pkgdef.capnp -mkdir -p ~/vagrant/bundle/opt/app/.sandstorm/ -cp ~/opt/app/.sandstorm/launcher.sh ~/vagrant/bundle/opt/app/.sandstorm/ -sed -i "s/\spgp/#pgp/g" sandstorm-pkgdef.capnp -spk pack $ROCKET_DEPLOY_DIR/rocket.chat-$ARTIFACT_NAME.spk diff --git a/.travis/snap.sh b/.travis/snap.sh index 52298eafea37..d322c6c52b0a 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=0.70.0-develop + RC_VERSION=1.4.0-develop fi echo "Preparing to trigger a snap release for $CHANNEL channel" diff --git a/.vscode/launch.json b/.vscode/launch.json index 05ddc20b9795..58b8073d89b2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,7 @@ { "version": "0.2.0", "configurations": [ + { "name": "Attach to meteor debug", "type": "node", @@ -10,7 +11,10 @@ "restart": false, "sourceMaps": true, "sourceMapPathOverrides": { - "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*" + "meteor://💻app/*": "${workspaceFolder}/*", + "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*", + "meteor://💻app/packages/chatpal:*": "${workspaceFolder}/packages/chatpal-*", + "meteor://💻app/packages/assistify:*": "${workspaceFolder}/packages/assistify-*" }, "protocol": "inspector" }, @@ -21,7 +25,10 @@ "url": "http://localhost:3000", "webRoot": "${workspaceFolder}", "sourceMapPathOverrides": { - "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*" + "meteor://💻app/*": "${workspaceFolder}/*", + "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*", + "meteor://💻app/packages/chatpal:*": "${workspaceFolder}/packages/chatpal-*", + "meteor://💻app/packages/assistify:*": "${workspaceFolder}/packages/assistify-*" } }, { @@ -36,7 +43,10 @@ "port": 9229, "timeout": 300000, //Rocket.Chat really takes some time to startup, so play it safe "sourceMapPathOverrides": { - "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*" + "meteor://💻app/*": "${workspaceFolder}/*", + "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*", + "meteor://💻app/packages/chatpal:*": "${workspaceFolder}/packages/chatpal-*", + "meteor://💻app/packages/assistify:*": "${workspaceFolder}/packages/assistify-*" }, "protocol": "inspector" }, @@ -52,7 +62,10 @@ "port": 9229, "timeout": 300000, //Rocket.Chat really takes some time to startup, so play it safe "sourceMapPathOverrides": { - "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*" + "meteor://💻app/*": "${workspaceFolder}/*", + "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*", + "meteor://💻app/packages/chatpal:*": "${workspaceFolder}/packages/chatpal-*", + "meteor://💻app/packages/assistify:*": "${workspaceFolder}/packages/assistify-*" }, "protocol": "inspector" }, @@ -68,7 +81,10 @@ "port": 9229, "timeout": 300000, //Rocket.Chat really takes some time to startup, so play it safe "sourceMapPathOverrides": { - "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*" + "meteor://💻app/*": "${workspaceFolder}/*", + "meteor://💻app/packages/rocketchat:*": "${workspaceFolder}/packages/rocketchat-*", + "meteor://💻app/packages/chatpal:*": "${workspaceFolder}/packages/chatpal-*", + "meteor://💻app/packages/assistify:*": "${workspaceFolder}/packages/assistify-*" }, "env": { "TEST_MODE": "true" diff --git a/HISTORY.md b/HISTORY.md index 99c6b3581a59..c565383bd7b1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,2187 @@ +# 1.3.0 +`2019-08-02 · 9 🎉 · 6 🚀 · 31 🐛 · 31 🔍 · 29 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🎉 New features + +- Show helpful error when oplog is missing ([#14954](https://github.com/RocketChat/Rocket.Chat/pull/14954) by [@justinr1234](https://github.com/justinr1234)) +- Subscription enabled marketplace ([#14948](https://github.com/RocketChat/Rocket.Chat/pull/14948)) +- Deprecate MongoDB version 3.2 ([#15025](https://github.com/RocketChat/Rocket.Chat/pull/15025)) +- Options to filter discussion and livechat on Admin > Rooms ([#15019](https://github.com/RocketChat/Rocket.Chat/pull/15019)) +- Settings to further customize GitLab OAuth ([#15014](https://github.com/RocketChat/Rocket.Chat/pull/15014)) +- Accept multiple redirect URIs on OAuth Apps ([#14935](https://github.com/RocketChat/Rocket.Chat/pull/14935)) +- Setting to configure custom authn context on SAML requests ([#14675](https://github.com/RocketChat/Rocket.Chat/pull/14675)) +- Webdav File Picker ([#14879](https://github.com/RocketChat/Rocket.Chat/pull/14879) by [@ubarsaiyan](https://github.com/ubarsaiyan)) +- Setting to prevent Livechat agents online when Office Hours are closed ([#14921](https://github.com/RocketChat/Rocket.Chat/pull/14921)) + +### 🚀 Improvements + +- Connectivity Services License Sync ([#15022](https://github.com/RocketChat/Rocket.Chat/pull/15022)) +- Add flag to identify remote federation users ([#15004](https://github.com/RocketChat/Rocket.Chat/pull/15004)) +- Extract federation config to its own file ([#14992](https://github.com/RocketChat/Rocket.Chat/pull/14992)) +- Update tabs markup ([#14964](https://github.com/RocketChat/Rocket.Chat/pull/14964)) +- Remove too specific helpers isFirefox() and isChrome() ([#14963](https://github.com/RocketChat/Rocket.Chat/pull/14963)) +- Add descriptions on user data download buttons and popup info ([#14852](https://github.com/RocketChat/Rocket.Chat/pull/14852)) + +### 🐛 Bug fixes + +- Russian grammatical errors ([#14622](https://github.com/RocketChat/Rocket.Chat/pull/14622) by [@BehindLoader](https://github.com/BehindLoader)) +- Message attachments not allowing float numbers ([#14412](https://github.com/RocketChat/Rocket.Chat/pull/14412)) +- Typo in german translation ([#14833](https://github.com/RocketChat/Rocket.Chat/pull/14833) by [@Le-onardo](https://github.com/Le-onardo)) +- users.setStatus REST endpoint not allowing reset status message ([#14916](https://github.com/RocketChat/Rocket.Chat/pull/14916)) +- SVG uploads crashing process ([#15006](https://github.com/RocketChat/Rocket.Chat/pull/15006) by [@snoopotic](https://github.com/snoopotic)) +- Edit message with arrow up key if not last message ([#15021](https://github.com/RocketChat/Rocket.Chat/pull/15021)) +- Livechat dashboard average and reaction time labels ([#14845](https://github.com/RocketChat/Rocket.Chat/pull/14845) by [@anandpathak](https://github.com/anandpathak)) +- Edit permissions screen ([#14950](https://github.com/RocketChat/Rocket.Chat/pull/14950)) +- Invite users auto complete cropping results ([#15020](https://github.com/RocketChat/Rocket.Chat/pull/15020)) +- Always displaying jumbomojis when using "marked" markdown ([#14861](https://github.com/RocketChat/Rocket.Chat/pull/14861)) +- CustomOauth Identity Step errors displayed in HTML format ([#15000](https://github.com/RocketChat/Rocket.Chat/pull/15000)) +- Custom User Status throttled by rate limiter ([#15001](https://github.com/RocketChat/Rocket.Chat/pull/15001)) +- Not being able to mention users with "all" and "here" usernames - do not allow users register that usernames ([#14468](https://github.com/RocketChat/Rocket.Chat/pull/14468) by [@hamidrezabstn](https://github.com/hamidrezabstn)) +- Users staying online after logout ([#14966](https://github.com/RocketChat/Rocket.Chat/pull/14966)) +- Chrome doesn't load additional search results when bottom is reached ([#14965](https://github.com/RocketChat/Rocket.Chat/pull/14965)) +- Wrong label order on room settings ([#14960](https://github.com/RocketChat/Rocket.Chat/pull/14960)) +- Allow storing the navigation history of unregistered Livechat visitors ([#14970](https://github.com/RocketChat/Rocket.Chat/pull/14970)) +- 50 custom emoji limit ([#14951](https://github.com/RocketChat/Rocket.Chat/pull/14951)) +- eternal loading file list ([#14952](https://github.com/RocketChat/Rocket.Chat/pull/14952)) +- load more messages ([#14967](https://github.com/RocketChat/Rocket.Chat/pull/14967)) +- Loading indicator positioning ([#14968](https://github.com/RocketChat/Rocket.Chat/pull/14968)) +- Jump to message missing in Starred Messages ([#14949](https://github.com/RocketChat/Rocket.Chat/pull/14949)) +- Method `getUsersOfRoom` not returning offline users if limit is not defined ([#14753](https://github.com/RocketChat/Rocket.Chat/pull/14753)) +- OTR key icon missing on messages ([#14953](https://github.com/RocketChat/Rocket.Chat/pull/14953)) +- Prevent error on trying insert message with duplicated id ([#14945](https://github.com/RocketChat/Rocket.Chat/pull/14945)) +- LDAP login with customField sync ([#14808](https://github.com/RocketChat/Rocket.Chat/pull/14808) by [@magicbelette](https://github.com/magicbelette)) +- Wrong custom status displayed on room leader panel ([#14958](https://github.com/RocketChat/Rocket.Chat/pull/14958)) +- Video recorder message echo ([#14671](https://github.com/RocketChat/Rocket.Chat/pull/14671) by [@vova-zush](https://github.com/vova-zush)) +- Opening Livechat messages on mobile apps ([#14785](https://github.com/RocketChat/Rocket.Chat/pull/14785) by [@zolbayars](https://github.com/zolbayars)) +- SAML login by giving displayName priority over userName for fullName ([#14880](https://github.com/RocketChat/Rocket.Chat/pull/14880) by [@pkolmann](https://github.com/pkolmann)) +- setupWizard calling multiple getSetupWizardParameters ([#15060](https://github.com/RocketChat/Rocket.Chat/pull/15060)) + +

+🔍 Minor changes + +- Wrong text when reporting a message ([#14515](https://github.com/RocketChat/Rocket.Chat/pull/14515) by [@zdumitru](https://github.com/zdumitru)) +- Add missing French translation ([#15013](https://github.com/RocketChat/Rocket.Chat/pull/15013) by [@commiaI](https://github.com/commiaI)) +- Fix statistics error for apps on first load ([#15026](https://github.com/RocketChat/Rocket.Chat/pull/15026)) +- Always convert the sha256 password to lowercase on checking ([#14941](https://github.com/RocketChat/Rocket.Chat/pull/14941)) +- New: Apps and integrations statistics ([#14878](https://github.com/RocketChat/Rocket.Chat/pull/14878)) +- improve: relocate some of wizard info to register ([#14884](https://github.com/RocketChat/Rocket.Chat/pull/14884)) +- Improve Docker compose readability ([#14457](https://github.com/RocketChat/Rocket.Chat/pull/14457) by [@NateScarlet](https://github.com/NateScarlet)) +- Bump marked from 0.5.2 to 0.6.1 ([#14969](https://github.com/RocketChat/Rocket.Chat/pull/14969) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Remove unused Meteor dependency (yasinuslu:blaze-meta) ([#14971](https://github.com/RocketChat/Rocket.Chat/pull/14971)) +- Bump photoswipe version to 4.1.3 ([#14977](https://github.com/RocketChat/Rocket.Chat/pull/14977)) +- Bump node-rsa version to 1.0.5 ([#14976](https://github.com/RocketChat/Rocket.Chat/pull/14976)) +- Bump juice version to 5.2.0 ([#14974](https://github.com/RocketChat/Rocket.Chat/pull/14974)) +- Remove unused dependency (lokijs) ([#14973](https://github.com/RocketChat/Rocket.Chat/pull/14973)) +- Regression: patch to improve emoji render ([#14980](https://github.com/RocketChat/Rocket.Chat/pull/14980)) +- [IMPROVEMENT] patch to improve emoji render ([#14722](https://github.com/RocketChat/Rocket.Chat/pull/14722)) +- Bump jquery from 3.3.1 to 3.4.0 in /packages/rocketchat-livechat/.app ([#14922](https://github.com/RocketChat/Rocket.Chat/pull/14922) by [@dependabot[bot]](https://github.com/dependabot[bot])) +- Callbacks perf ([#14915](https://github.com/RocketChat/Rocket.Chat/pull/14915)) +- Split oplog emitters in files ([#14917](https://github.com/RocketChat/Rocket.Chat/pull/14917)) +- Extract canSendMessage function ([#14909](https://github.com/RocketChat/Rocket.Chat/pull/14909)) +- Improve: Get public key for marketplace ([#14851](https://github.com/RocketChat/Rocket.Chat/pull/14851)) +- Merge master into develop & Set version to 1.3.0-develop ([#14889](https://github.com/RocketChat/Rocket.Chat/pull/14889)) +- Regression: fix code style, setup wizard error and profile page header ([#15041](https://github.com/RocketChat/Rocket.Chat/pull/15041)) +- Regression: Framework version being attached to a request that doesn't require it ([#15039](https://github.com/RocketChat/Rocket.Chat/pull/15039)) +- Update Livechat widget ([#15046](https://github.com/RocketChat/Rocket.Chat/pull/15046)) +- Regression: getSetupWizardParameters ([#15067](https://github.com/RocketChat/Rocket.Chat/pull/15067)) +- Regression: Webdav File Picker search and fixed overflows ([#15027](https://github.com/RocketChat/Rocket.Chat/pull/15027) by [@ubarsaiyan](https://github.com/ubarsaiyan)) +- Regression: Improve apps bridges for HA setup ([#15080](https://github.com/RocketChat/Rocket.Chat/pull/15080)) +- Regression: displaying errors for apps not installed from Marketplace ([#15075](https://github.com/RocketChat/Rocket.Chat/pull/15075)) +- Regression: Marketplace app pricing plan description ([#15076](https://github.com/RocketChat/Rocket.Chat/pull/15076)) +- Regression: uninstall subscribed app modal ([#15077](https://github.com/RocketChat/Rocket.Chat/pull/15077)) +- Regression: Apps and Marketplace UI issues ([#15045](https://github.com/RocketChat/Rocket.Chat/pull/15045)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@BehindLoader](https://github.com/BehindLoader) +- [@Le-onardo](https://github.com/Le-onardo) +- [@NateScarlet](https://github.com/NateScarlet) +- [@anandpathak](https://github.com/anandpathak) +- [@commiaI](https://github.com/commiaI) +- [@dependabot[bot]](https://github.com/dependabot[bot]) +- [@hamidrezabstn](https://github.com/hamidrezabstn) +- [@justinr1234](https://github.com/justinr1234) +- [@magicbelette](https://github.com/magicbelette) +- [@pkolmann](https://github.com/pkolmann) +- [@snoopotic](https://github.com/snoopotic) +- [@ubarsaiyan](https://github.com/ubarsaiyan) +- [@vova-zush](https://github.com/vova-zush) +- [@zdumitru](https://github.com/zdumitru) +- [@zolbayars](https://github.com/zolbayars) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@alansikora](https://github.com/alansikora) +- [@brakhane](https://github.com/brakhane) +- [@cardoso](https://github.com/cardoso) +- [@d-gubert](https://github.com/d-gubert) +- [@engelgabriel](https://github.com/engelgabriel) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@graywolf336](https://github.com/graywolf336) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 1.2.2 +`2019-07-29` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +# 1.2.1 +`2019-06-28 · 1 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- Not showing local app on App Details ([#14894](https://github.com/RocketChat/Rocket.Chat/pull/14894)) + +
+🔍 Minor changes + +- Release 1.2.1 ([#14898](https://github.com/RocketChat/Rocket.Chat/pull/14898)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@d-gubert](https://github.com/d-gubert) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 1.2.0 +`2019-06-27 · 8 🎉 · 4 🚀 · 12 🐛 · 8 🔍 · 21 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🎉 New features + +- Custom User Status ([#13933](https://github.com/RocketChat/Rocket.Chat/pull/13933) by [@wreiske](https://github.com/wreiske)) +- changed mongo version for snap from 3.2.7 to 3.4.20 ([#14838](https://github.com/RocketChat/Rocket.Chat/pull/14838)) +- Add loading animation to webdav file picker ([#14759](https://github.com/RocketChat/Rocket.Chat/pull/14759) by [@ubarsaiyan](https://github.com/ubarsaiyan)) +- Add tmid property to outgoing integration ([#14699](https://github.com/RocketChat/Rocket.Chat/pull/14699)) +- Endpoint to anonymously read channel's messages ([#14714](https://github.com/RocketChat/Rocket.Chat/pull/14714)) +- Add Livechat inquiries endpoints ([#14779](https://github.com/RocketChat/Rocket.Chat/pull/14779)) +- Configuration to limit amount of livechat inquiries displayed ([#14690](https://github.com/RocketChat/Rocket.Chat/pull/14690)) +- Show App bundles and its apps ([#14886](https://github.com/RocketChat/Rocket.Chat/pull/14886)) + +### 🚀 Improvements + +- Adds link to download generated user data file ([#14175](https://github.com/RocketChat/Rocket.Chat/pull/14175)) +- Layout of livechat manager pages to new style ([#13900](https://github.com/RocketChat/Rocket.Chat/pull/13900)) +- Add an optional rocketchat-protocol DNS entry for Federation ([#14589](https://github.com/RocketChat/Rocket.Chat/pull/14589)) +- Use configurable colors on sidebar items ([#14624](https://github.com/RocketChat/Rocket.Chat/pull/14624)) + +### 🐛 Bug fixes + +- Error when using Download My Data or Export My Data ([#14645](https://github.com/RocketChat/Rocket.Chat/pull/14645)) +- Removes E2E action button, icon and banner when E2E is disabled. ([#14810](https://github.com/RocketChat/Rocket.Chat/pull/14810)) +- Assume microphone is available ([#14710](https://github.com/RocketChat/Rocket.Chat/pull/14710)) +- Move the set Avatar call on user creation to make sure the user has username ([#14665](https://github.com/RocketChat/Rocket.Chat/pull/14665)) +- users typing forever ([#14724](https://github.com/RocketChat/Rocket.Chat/pull/14724)) +- Increasing time to rate limit in shield.svg endpoint and add a setting to disable API rate limiter ([#14709](https://github.com/RocketChat/Rocket.Chat/pull/14709)) +- Wrong filter field when filtering current Livechats ([#14569](https://github.com/RocketChat/Rocket.Chat/pull/14569)) +- Import Chart.js error ([#14471](https://github.com/RocketChat/Rocket.Chat/pull/14471) by [@sonbn0](https://github.com/sonbn0)) +- Name is undefined in some emails ([#14533](https://github.com/RocketChat/Rocket.Chat/pull/14533)) +- Direct reply delete config and description ([#14493](https://github.com/RocketChat/Rocket.Chat/pull/14493) by [@ruKurz](https://github.com/ruKurz)) +- Custom status fixes ([#14853](https://github.com/RocketChat/Rocket.Chat/pull/14853) by [@wreiske](https://github.com/wreiske)) +- LinkedIn OAuth login ([#14887](https://github.com/RocketChat/Rocket.Chat/pull/14887)) + +
+🔍 Minor changes + +- Fix not fully extracted pieces ([#14805](https://github.com/RocketChat/Rocket.Chat/pull/14805)) +- Regression: Fix file upload ([#14804](https://github.com/RocketChat/Rocket.Chat/pull/14804)) +- Extract permissions functions ([#14777](https://github.com/RocketChat/Rocket.Chat/pull/14777)) +- Add custom fileupload whitelist property ([#14754](https://github.com/RocketChat/Rocket.Chat/pull/14754)) +- Merge master into develop & Set version to 1.2.0-develop ([#14656](https://github.com/RocketChat/Rocket.Chat/pull/14656) by [@AnBo83](https://github.com/AnBo83) & [@knrt10](https://github.com/knrt10) & [@lolimay](https://github.com/lolimay) & [@mohamedar97](https://github.com/mohamedar97) & [@thaiphv](https://github.com/thaiphv)) +- Regression: Fix desktop notifications not being sent ([#14860](https://github.com/RocketChat/Rocket.Chat/pull/14860)) +- Regression: Allow debugging of cached collections by name ([#14862](https://github.com/RocketChat/Rocket.Chat/pull/14862)) +- Allow debugging of cached collections by name ([#14859](https://github.com/RocketChat/Rocket.Chat/pull/14859)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@AnBo83](https://github.com/AnBo83) +- [@knrt10](https://github.com/knrt10) +- [@lolimay](https://github.com/lolimay) +- [@mohamedar97](https://github.com/mohamedar97) +- [@ruKurz](https://github.com/ruKurz) +- [@sonbn0](https://github.com/sonbn0) +- [@thaiphv](https://github.com/thaiphv) +- [@ubarsaiyan](https://github.com/ubarsaiyan) +- [@wreiske](https://github.com/wreiske) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@LuluGO](https://github.com/LuluGO) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@PrajvalRaval](https://github.com/PrajvalRaval) +- [@alansikora](https://github.com/alansikora) +- [@engelgabriel](https://github.com/engelgabriel) +- [@ggazzo](https://github.com/ggazzo) +- [@marceloschmidt](https://github.com/marceloschmidt) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 1.1.4 +`2019-07-29` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +# 1.1.3 +`2019-06-21 · 1 🐛 · 2 🔍 · 2 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- Gap of messages when loading history when using threads ([#14837](https://github.com/RocketChat/Rocket.Chat/pull/14837)) + +
+🔍 Minor changes + +- Release 1.1.3 ([#14850](https://github.com/RocketChat/Rocket.Chat/pull/14850)) +- Regression: thread loading parent msg if is not loaded ([#14839](https://github.com/RocketChat/Rocket.Chat/pull/14839)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@ggazzo](https://github.com/ggazzo) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 1.1.2 +`2019-06-17 · 3 🐛 · 1 🔍 · 4 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- User status information on User Info panel ([#14763](https://github.com/RocketChat/Rocket.Chat/pull/14763)) +- User Real Name being erased when not modified ([#14711](https://github.com/RocketChat/Rocket.Chat/pull/14711)) +- Anonymous chat read ([#14717](https://github.com/RocketChat/Rocket.Chat/pull/14717)) + +
+🔍 Minor changes + +- Release 1.1.2 ([#14823](https://github.com/RocketChat/Rocket.Chat/pull/14823)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@ggazzo](https://github.com/ggazzo) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 1.1.1 +`2019-05-30 · 2 🐛 · 1 🔍 · 3 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- SAML login error. ([#14686](https://github.com/RocketChat/Rocket.Chat/pull/14686)) +- Load messages after disconnect and message box scroll missing ([#14668](https://github.com/RocketChat/Rocket.Chat/pull/14668)) + +
+🔍 Minor changes + +- Removing unnecesary federation configs ([#14674](https://github.com/RocketChat/Rocket.Chat/pull/14674)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@alansikora](https://github.com/alansikora) +- [@ggazzo](https://github.com/ggazzo) + +# 1.1.0 +`2019-05-27 · 5 🎉 · 10 🚀 · 52 🐛 · 35 🔍 · 27 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🎉 New features + +- Setting option to mark as containing a secret/password ([#10273](https://github.com/RocketChat/Rocket.Chat/pull/10273)) +- Custom user name field from Custom OAuth ([#14381](https://github.com/RocketChat/Rocket.Chat/pull/14381) by [@mjovanovic0](https://github.com/mjovanovic0)) +- Add pause and reset button when adding custom sound ([#13615](https://github.com/RocketChat/Rocket.Chat/pull/13615) by [@knrt10](https://github.com/knrt10)) +- Missing "view-outside-room_description" translation key ([#13680](https://github.com/RocketChat/Rocket.Chat/pull/13680) by [@bhardwajaditya](https://github.com/bhardwajaditya)) +- Returns custom emojis through the Livechat REST API ([#14370](https://github.com/RocketChat/Rocket.Chat/pull/14370)) + +### 🚀 Improvements + +- Message rendering time ([#14252](https://github.com/RocketChat/Rocket.Chat/pull/14252)) +- Change user presence events to Meteor Streams ([#14488](https://github.com/RocketChat/Rocket.Chat/pull/14488)) +- Upgrade EmojiOne to JoyPixels 4.5.0 ([#13807](https://github.com/RocketChat/Rocket.Chat/pull/13807) by [@wreiske](https://github.com/wreiske)) +- Don't show unread count badge in burger menu if it is from the opened room ([#12971](https://github.com/RocketChat/Rocket.Chat/pull/12971)) +- Livechat CRM secret token optional ([#14022](https://github.com/RocketChat/Rocket.Chat/pull/14022)) +- jump to selected message on open thread ([#14460](https://github.com/RocketChat/Rocket.Chat/pull/14460)) +- Don't use regex to find users ([#14397](https://github.com/RocketChat/Rocket.Chat/pull/14397)) +- Added flag `skipActiveUsersToBeReady` to not wait the load of `active users` to present the Web interface ([#14431](https://github.com/RocketChat/Rocket.Chat/pull/14431)) +- SAML login process refactoring ([#12891](https://github.com/RocketChat/Rocket.Chat/pull/12891) by [@kukkjanos](https://github.com/kukkjanos)) +- Allow change Discussion's properties ([#14389](https://github.com/RocketChat/Rocket.Chat/pull/14389)) + +### 🐛 Bug fixes + +- Downloading files when running in sub directory ([#14485](https://github.com/RocketChat/Rocket.Chat/pull/14485) by [@miolane](https://github.com/miolane)) +- Broken layout when sidebar is open on IE/Edge ([#14567](https://github.com/RocketChat/Rocket.Chat/pull/14567)) +- Channel names on Directory got cut on small screens ([#14542](https://github.com/RocketChat/Rocket.Chat/pull/14542)) +- Duplicated link to jump to message ([#14505](https://github.com/RocketChat/Rocket.Chat/pull/14505)) +- Edit Message when down arrow is pressed. ([#14369](https://github.com/RocketChat/Rocket.Chat/pull/14369) by [@Kailash0311](https://github.com/Kailash0311)) +- Unread property of the room's lastMessage object was being wrong some times ([#13919](https://github.com/RocketChat/Rocket.Chat/pull/13919)) +- Multiple Slack Importer Bugs ([#12084](https://github.com/RocketChat/Rocket.Chat/pull/12084)) +- No feedback when adding users that already exists in a room ([#14534](https://github.com/RocketChat/Rocket.Chat/pull/14534) by [@gsunit](https://github.com/gsunit)) +- Custom scripts descriptions were not clear enough ([#14516](https://github.com/RocketChat/Rocket.Chat/pull/14516)) +- Role `user` has being added after email verification even for non anonymous users ([#14263](https://github.com/RocketChat/Rocket.Chat/pull/14263)) +- Several problems with read-only rooms and muted users ([#11311](https://github.com/RocketChat/Rocket.Chat/pull/11311)) +- Channel settings form to textarea for Topic and Description ([#13328](https://github.com/RocketChat/Rocket.Chat/pull/13328) by [@supra08](https://github.com/supra08)) +- Elements in User Info require some padding ([#13640](https://github.com/RocketChat/Rocket.Chat/pull/13640) by [@mushroomgenie](https://github.com/mushroomgenie)) +- Showing the id instead of the name of custom notification sound ([#13660](https://github.com/RocketChat/Rocket.Chat/pull/13660) by [@knrt10](https://github.com/knrt10)) +- Remove Livechat guest data was removing more rooms than expected ([#14509](https://github.com/RocketChat/Rocket.Chat/pull/14509)) +- Save custom emoji with special characters causes some errors ([#14456](https://github.com/RocketChat/Rocket.Chat/pull/14456)) +- Verify if the user is requesting your own information in users.info ([#14242](https://github.com/RocketChat/Rocket.Chat/pull/14242)) +- RocketChat client sending out video call requests unnecessarily ([#14496](https://github.com/RocketChat/Rocket.Chat/pull/14496)) +- `Alphabetical` translation in DE ([#14490](https://github.com/RocketChat/Rocket.Chat/pull/14490) by [@AnBo83](https://github.com/AnBo83)) +- Fix redirect to First channel after login ([#14434](https://github.com/RocketChat/Rocket.Chat/pull/14434)) +- Ignored messages ([#14465](https://github.com/RocketChat/Rocket.Chat/pull/14465)) +- Allow data URLs in isURL/getURL helpers ([#14464](https://github.com/RocketChat/Rocket.Chat/pull/14464)) +- You must join to view messages in this channel ([#14461](https://github.com/RocketChat/Rocket.Chat/pull/14461)) +- Channel Leader Bar is in the way of Thread Header ([#14443](https://github.com/RocketChat/Rocket.Chat/pull/14443)) +- Discussion name being invalid ([#14442](https://github.com/RocketChat/Rocket.Chat/pull/14442)) +- Room name was undefined in some info dialogs ([#14415](https://github.com/RocketChat/Rocket.Chat/pull/14415)) +- Exception on crowd sync due to a wrong logging method ([#14405](https://github.com/RocketChat/Rocket.Chat/pull/14405)) +- IE11 support ([#14422](https://github.com/RocketChat/Rocket.Chat/pull/14422)) +- Escape unrecognized slash command message ([#14432](https://github.com/RocketChat/Rocket.Chat/pull/14432)) +- Mentions message missing 'jump to message' action ([#14430](https://github.com/RocketChat/Rocket.Chat/pull/14430)) +- preview pdf its not working ([#14419](https://github.com/RocketChat/Rocket.Chat/pull/14419)) +- Messages on thread panel were receiving wrong context/subscription ([#14404](https://github.com/RocketChat/Rocket.Chat/pull/14404)) +- Error 400 on send a reply to an old thread ([#14402](https://github.com/RocketChat/Rocket.Chat/pull/14402)) +- Users actions in administration were returning error ([#14400](https://github.com/RocketChat/Rocket.Chat/pull/14400)) +- Fallback to mongo version that doesn't require clusterMonitor role ([#14403](https://github.com/RocketChat/Rocket.Chat/pull/14403)) +- SAML credentialToken removal was preventing mobile from being able to authenticate ([#14345](https://github.com/RocketChat/Rocket.Chat/pull/14345)) +- Stream not connecting connect when using subdir and multi-instance ([#14376](https://github.com/RocketChat/Rocket.Chat/pull/14376)) +- Pressing Enter in User Search field at channel causes reload ([#14388](https://github.com/RocketChat/Rocket.Chat/pull/14388)) +- Wrong token name was generating error on Gitlab OAuth login ([#14379](https://github.com/RocketChat/Rocket.Chat/pull/14379)) +- more message actions to threads context(follow, unfollow, copy, delete) ([#14387](https://github.com/RocketChat/Rocket.Chat/pull/14387)) +- Unnecessary meteor.defer on openRoom ([#14396](https://github.com/RocketChat/Rocket.Chat/pull/14396)) +- Messages on threads disappearing ([#14393](https://github.com/RocketChat/Rocket.Chat/pull/14393)) +- Bell was too small on threads ([#14394](https://github.com/RocketChat/Rocket.Chat/pull/14394)) +- Main thread title on replies ([#14372](https://github.com/RocketChat/Rocket.Chat/pull/14372)) +- New day separator overlapping above system message ([#14362](https://github.com/RocketChat/Rocket.Chat/pull/14362)) +- E2E messages not decrypting in message threads ([#14580](https://github.com/RocketChat/Rocket.Chat/pull/14580)) +- Send replyTo for livechat offline messages ([#14568](https://github.com/RocketChat/Rocket.Chat/pull/14568)) +- Mailer breaking if user doesn't have an email address ([#14614](https://github.com/RocketChat/Rocket.Chat/pull/14614)) +- Role name spacing on Permissions page ([#14625](https://github.com/RocketChat/Rocket.Chat/pull/14625)) +- Avatar images on old Livechat client ([#14590](https://github.com/RocketChat/Rocket.Chat/pull/14590) by [@arminfelder](https://github.com/arminfelder)) +- Inject code at the end of tag ([#14623](https://github.com/RocketChat/Rocket.Chat/pull/14623)) +- "Blank page" on safari 10.x ([#14651](https://github.com/RocketChat/Rocket.Chat/pull/14651)) + +
+🔍 Minor changes + +- Removed unnecessary DDP unblocks ([#13641](https://github.com/RocketChat/Rocket.Chat/pull/13641)) +- Fix emoji replacing some chars ([#14570](https://github.com/RocketChat/Rocket.Chat/pull/14570)) +- LingoHub based on develop ([#14561](https://github.com/RocketChat/Rocket.Chat/pull/14561)) +- Refactor WebRTC class ([#13736](https://github.com/RocketChat/Rocket.Chat/pull/13736)) +- Update Meteor Streamer package ([#14551](https://github.com/RocketChat/Rocket.Chat/pull/14551)) +- Regression: unit tests were being skipped ([#14543](https://github.com/RocketChat/Rocket.Chat/pull/14543)) +- MsgTyping refactor ([#14495](https://github.com/RocketChat/Rocket.Chat/pull/14495)) +- Google Plus account is no longer accessible ([#14503](https://github.com/RocketChat/Rocket.Chat/pull/14503) by [@zdumitru](https://github.com/zdumitru)) +- [IMPROVEMENT] Add tooltip to to notify user the purpose of back button in discussion ([#13872](https://github.com/RocketChat/Rocket.Chat/pull/13872) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- eslint errors currently on develop ([#14518](https://github.com/RocketChat/Rocket.Chat/pull/14518)) +- Allow removing description, topic and annoucement of rooms(set as empty string) ([#13682](https://github.com/RocketChat/Rocket.Chat/pull/13682)) +- [IMPROVEMENT] Don't group messages with different alias ([#14257](https://github.com/RocketChat/Rocket.Chat/pull/14257) by [@jungeonkim](https://github.com/jungeonkim)) +- LingoHub based on develop ([#14478](https://github.com/RocketChat/Rocket.Chat/pull/14478)) +- Remove specific eslint rules ([#14459](https://github.com/RocketChat/Rocket.Chat/pull/14459)) +- New eslint rules ([#14332](https://github.com/RocketChat/Rocket.Chat/pull/14332)) +- Fix i18n files keys sort ([#14433](https://github.com/RocketChat/Rocket.Chat/pull/14433)) +- Fixes on DAU and MAU aggregations ([#14418](https://github.com/RocketChat/Rocket.Chat/pull/14418)) +- Add missing german translations ([#14386](https://github.com/RocketChat/Rocket.Chat/pull/14386)) +- LingoHub based on develop ([#14426](https://github.com/RocketChat/Rocket.Chat/pull/14426)) +- fix discussions: remove restriction for editing room info, server side ([#14039](https://github.com/RocketChat/Rocket.Chat/pull/14039)) +- Fix: Message body was not being updated when user disabled nrr message ([#14390](https://github.com/RocketChat/Rocket.Chat/pull/14390)) +- Improve German translations ([#14351](https://github.com/RocketChat/Rocket.Chat/pull/14351)) +- Merge master into develop & Set version to 1.1.0-develop ([#14317](https://github.com/RocketChat/Rocket.Chat/pull/14317) by [@wreiske](https://github.com/wreiske)) +- Merge master into develop & Set version to 1.1.0-develop ([#14294](https://github.com/RocketChat/Rocket.Chat/pull/14294)) +- Fix: Add emoji shortnames to emoji's list ([#14576](https://github.com/RocketChat/Rocket.Chat/pull/14576)) +- Ci improvements ([#14600](https://github.com/RocketChat/Rocket.Chat/pull/14600)) +- Fix: emoji render performance for alias ([#14593](https://github.com/RocketChat/Rocket.Chat/pull/14593)) +- Federation i18n message changes ([#14595](https://github.com/RocketChat/Rocket.Chat/pull/14595)) +- [REGRESSION] Fix Slack bridge channel owner on channel creation ([#14565](https://github.com/RocketChat/Rocket.Chat/pull/14565)) +- Fix thumbs up emoji shortname ([#14581](https://github.com/RocketChat/Rocket.Chat/pull/14581)) +- [Fix] broken logo url in app.json ([#14572](https://github.com/RocketChat/Rocket.Chat/pull/14572) by [@jaredmoody](https://github.com/jaredmoody)) +- Add digitalocean button to readme ([#14583](https://github.com/RocketChat/Rocket.Chat/pull/14583)) +- Improvement: Permissions table ([#14646](https://github.com/RocketChat/Rocket.Chat/pull/14646)) +- Regression: Handle missing emojis ([#14641](https://github.com/RocketChat/Rocket.Chat/pull/14641)) +- LingoHub based on develop ([#14643](https://github.com/RocketChat/Rocket.Chat/pull/14643)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@AnBo83](https://github.com/AnBo83) +- [@Kailash0311](https://github.com/Kailash0311) +- [@arminfelder](https://github.com/arminfelder) +- [@ashwaniYDV](https://github.com/ashwaniYDV) +- [@bhardwajaditya](https://github.com/bhardwajaditya) +- [@gsunit](https://github.com/gsunit) +- [@jaredmoody](https://github.com/jaredmoody) +- [@jungeonkim](https://github.com/jungeonkim) +- [@knrt10](https://github.com/knrt10) +- [@kukkjanos](https://github.com/kukkjanos) +- [@miolane](https://github.com/miolane) +- [@mjovanovic0](https://github.com/mjovanovic0) +- [@mushroomgenie](https://github.com/mushroomgenie) +- [@supra08](https://github.com/supra08) +- [@wreiske](https://github.com/wreiske) +- [@zdumitru](https://github.com/zdumitru) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@alansikora](https://github.com/alansikora) +- [@engelgabriel](https://github.com/engelgabriel) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@mrsimpson](https://github.com/mrsimpson) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 1.0.4 +`2019-07-29 · 1 🐛 · 1 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- Not sanitized message types ([#15054](https://github.com/RocketChat/Rocket.Chat/pull/15054)) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@ggazzo](https://github.com/ggazzo) + +# 1.0.3 +`2019-05-09 · 1 🔍 · 8 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +
+🔍 Minor changes + +- Release 1.0.3 ([#14446](https://github.com/RocketChat/Rocket.Chat/pull/14446)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@engelgabriel](https://github.com/engelgabriel) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@mrsimpson](https://github.com/mrsimpson) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 1.0.2 +`2019-04-30 · 2 🚀 · 8 🐛 · 6 🔍 · 10 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🚀 Improvements + +- i18n of threads and discussion buttons ([#14334](https://github.com/RocketChat/Rocket.Chat/pull/14334)) +- Better error message when not able to get MongoDB Version ([#14320](https://github.com/RocketChat/Rocket.Chat/pull/14320)) + +### 🐛 Bug fixes + +- Unread line and new day separator were not aligned ([#14338](https://github.com/RocketChat/Rocket.Chat/pull/14338)) +- Audio notification for messages on DM ([#14336](https://github.com/RocketChat/Rocket.Chat/pull/14336)) +- Duplicate thread message after editing ([#14330](https://github.com/RocketChat/Rocket.Chat/pull/14330)) +- New day separator rendered over thread reply ([#14328](https://github.com/RocketChat/Rocket.Chat/pull/14328)) +- Missing i18n for some new Permissions ([#14011](https://github.com/RocketChat/Rocket.Chat/pull/14011) by [@lolimay](https://github.com/lolimay)) +- View Logs admin page was broken and not rendering color logs ([#14316](https://github.com/RocketChat/Rocket.Chat/pull/14316)) +- show roles on message ([#14313](https://github.com/RocketChat/Rocket.Chat/pull/14313)) +- Remove reference to inexistent field when deleting message in thread ([#14311](https://github.com/RocketChat/Rocket.Chat/pull/14311)) + +
+🔍 Minor changes + +- Release 1.0.2 ([#14339](https://github.com/RocketChat/Rocket.Chat/pull/14339) by [@AnBo83](https://github.com/AnBo83) & [@knrt10](https://github.com/knrt10) & [@lolimay](https://github.com/lolimay) & [@mohamedar97](https://github.com/mohamedar97) & [@thaiphv](https://github.com/thaiphv)) +- Add cross-browser select arrow positioning ([#14318](https://github.com/RocketChat/Rocket.Chat/pull/14318)) +- i18n: Update German strings ([#14182](https://github.com/RocketChat/Rocket.Chat/pull/14182) by [@AnBo83](https://github.com/AnBo83)) +- [Regression] Anonymous user fix ([#14301](https://github.com/RocketChat/Rocket.Chat/pull/14301) by [@knrt10](https://github.com/knrt10)) +- Coerces the MongoDB version string ([#14299](https://github.com/RocketChat/Rocket.Chat/pull/14299) by [@thaiphv](https://github.com/thaiphv)) +- [Fix] group name appears instead of the room id ([#14075](https://github.com/RocketChat/Rocket.Chat/pull/14075) by [@mohamedar97](https://github.com/mohamedar97)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@AnBo83](https://github.com/AnBo83) +- [@knrt10](https://github.com/knrt10) +- [@lolimay](https://github.com/lolimay) +- [@mohamedar97](https://github.com/mohamedar97) +- [@thaiphv](https://github.com/thaiphv) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@d-gubert](https://github.com/d-gubert) +- [@ggazzo](https://github.com/ggazzo) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 1.0.1 +`2019-04-28 · 7 🐛 · 4 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- Popup cloud console in new window ([#14296](https://github.com/RocketChat/Rocket.Chat/pull/14296)) +- Switch oplog required doc link to more accurate link ([#14288](https://github.com/RocketChat/Rocket.Chat/pull/14288)) +- Optional exit on Unhandled Promise Rejection ([#14291](https://github.com/RocketChat/Rocket.Chat/pull/14291)) +- Error when accessing avatar with no token ([#14293](https://github.com/RocketChat/Rocket.Chat/pull/14293)) +- Startup error in registration check ([#14286](https://github.com/RocketChat/Rocket.Chat/pull/14286)) +- Wrong header at Apps admin section ([#14290](https://github.com/RocketChat/Rocket.Chat/pull/14290)) +- Error when accessing an invalid file upload url ([#14282](https://github.com/RocketChat/Rocket.Chat/pull/14282) by [@wreiske](https://github.com/wreiske)) + +### 👩‍💻👨‍💻 Contributors 😍 + +- [@wreiske](https://github.com/wreiske) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@d-gubert](https://github.com/d-gubert) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@rodrigok](https://github.com/rodrigok) + +# 1.0.0 +`2019-04-28 · 4 ️️️⚠️ · 30 🎉 · 32 🚀 · 97 🐛 · 173 🔍 · 60 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### ⚠️ BREAKING CHANGES + +- Remove deprecated file upload engine Slingshot ([#13724](https://github.com/RocketChat/Rocket.Chat/pull/13724)) +- Remove internal hubot package ([#13522](https://github.com/RocketChat/Rocket.Chat/pull/13522)) +- Prevent start if incompatible mongo version ([#13927](https://github.com/RocketChat/Rocket.Chat/pull/13927)) +- Require OPLOG/REPLICASET to run Rocket.Chat ([#14227](https://github.com/RocketChat/Rocket.Chat/pull/14227)) + +### 🎉 New features + +- Marketplace integration with Rocket.Chat Cloud ([#13809](https://github.com/RocketChat/Rocket.Chat/pull/13809)) +- Add message action to copy message to input as reply ([#12626](https://github.com/RocketChat/Rocket.Chat/pull/12626)) +- Allow sending long messages as attachments ([#13819](https://github.com/RocketChat/Rocket.Chat/pull/13819)) +- Add e-mail field on Livechat Departments ([#13775](https://github.com/RocketChat/Rocket.Chat/pull/13775)) +- Provide new Livechat client as community feature ([#13723](https://github.com/RocketChat/Rocket.Chat/pull/13723)) +- Discussions ([#13541](https://github.com/RocketChat/Rocket.Chat/pull/13541) by [@vickyokrm](https://github.com/vickyokrm)) +- Bosnian lang (BS) ([#13635](https://github.com/RocketChat/Rocket.Chat/pull/13635) by [@fliptrail](https://github.com/fliptrail)) +- Federation ([#12370](https://github.com/RocketChat/Rocket.Chat/pull/12370)) +- Show department field on Livechat visitor panel ([#13530](https://github.com/RocketChat/Rocket.Chat/pull/13530)) +- Add offset parameter to channels.history, groups.history, dm.history ([#13310](https://github.com/RocketChat/Rocket.Chat/pull/13310) by [@xbolshe](https://github.com/xbolshe)) +- Permission to assign roles ([#13597](https://github.com/RocketChat/Rocket.Chat/pull/13597)) +- reply with a file ([#12095](https://github.com/RocketChat/Rocket.Chat/pull/12095) by [@rssilva](https://github.com/rssilva)) +- legal notice page ([#12472](https://github.com/RocketChat/Rocket.Chat/pull/12472) by [@localguru](https://github.com/localguru)) +- Add missing remove add leader channel ([#13315](https://github.com/RocketChat/Rocket.Chat/pull/13315) by [@Montel](https://github.com/Montel)) +- users.setActiveStatus endpoint in rest api ([#13443](https://github.com/RocketChat/Rocket.Chat/pull/13443) by [@thayannevls](https://github.com/thayannevls)) +- User avatars from external source ([#7929](https://github.com/RocketChat/Rocket.Chat/pull/7929) by [@mjovanovic0](https://github.com/mjovanovic0)) +- Add an option to delete file in files list ([#13815](https://github.com/RocketChat/Rocket.Chat/pull/13815)) +- Threads V 1.0 ([#13996](https://github.com/RocketChat/Rocket.Chat/pull/13996)) +- Add support to updatedSince parameter in emoji-custom.list and deprecated old endpoint ([#13510](https://github.com/RocketChat/Rocket.Chat/pull/13510)) +- Chatpal: Enable custom search parameters ([#13829](https://github.com/RocketChat/Rocket.Chat/pull/13829) by [@Peym4n](https://github.com/Peym4n)) +- - Add setting to request a comment when closing Livechat room ([#13983](https://github.com/RocketChat/Rocket.Chat/pull/13983) by [@knrt10](https://github.com/knrt10)) +- Rest threads ([#14045](https://github.com/RocketChat/Rocket.Chat/pull/14045)) +- Add GET method to fetch Livechat message through REST API ([#14147](https://github.com/RocketChat/Rocket.Chat/pull/14147)) +- Add Voxtelesys to list of SMS providers ([#13697](https://github.com/RocketChat/Rocket.Chat/pull/13697) by [@jhnburke8](https://github.com/jhnburke8) & [@john08burke](https://github.com/john08burke)) +- Rest endpoints of discussions ([#13987](https://github.com/RocketChat/Rocket.Chat/pull/13987)) +- Multiple slackbridges ([#11346](https://github.com/RocketChat/Rocket.Chat/pull/11346) by [@kable-wilmoth](https://github.com/kable-wilmoth)) +- option to not use nrr (experimental) ([#14224](https://github.com/RocketChat/Rocket.Chat/pull/14224)) +- Set up livechat connections created from new client ([#14236](https://github.com/RocketChat/Rocket.Chat/pull/14236)) +- allow drop files on thread ([#14214](https://github.com/RocketChat/Rocket.Chat/pull/14214)) +- Update message actions ([#14268](https://github.com/RocketChat/Rocket.Chat/pull/14268)) + +### 🚀 Improvements + +- UI of page not found ([#13757](https://github.com/RocketChat/Rocket.Chat/pull/13757) by [@fliptrail](https://github.com/fliptrail)) +- Show rooms with mentions on unread category even with hide counter ([#13948](https://github.com/RocketChat/Rocket.Chat/pull/13948)) +- Join channels by sending a message or join button (#13752) ([#13752](https://github.com/RocketChat/Rocket.Chat/pull/13752) by [@bhardwajaditya](https://github.com/bhardwajaditya)) +- Filter agents with autocomplete input instead of select element ([#13730](https://github.com/RocketChat/Rocket.Chat/pull/13730)) +- Ignore agent status when queuing incoming livechats via Guest Pool ([#13818](https://github.com/RocketChat/Rocket.Chat/pull/13818)) +- Replaces color #13679A to #1d74f5 ([#13796](https://github.com/RocketChat/Rocket.Chat/pull/13796) by [@fliptrail](https://github.com/fliptrail)) +- Remove unnecessary "File Upload". ([#13743](https://github.com/RocketChat/Rocket.Chat/pull/13743) by [@knrt10](https://github.com/knrt10)) +- Add index for room's ts ([#13726](https://github.com/RocketChat/Rocket.Chat/pull/13726)) +- Add decoding for commonName (cn) and displayName attributes for SAML ([#12347](https://github.com/RocketChat/Rocket.Chat/pull/12347) by [@pkolmann](https://github.com/pkolmann)) +- Deprecate fixCordova helper ([#13598](https://github.com/RocketChat/Rocket.Chat/pull/13598)) +- Remove dangling side-nav styles ([#13584](https://github.com/RocketChat/Rocket.Chat/pull/13584)) +- Disable X-Powered-By header in all known express middlewares ([#13388](https://github.com/RocketChat/Rocket.Chat/pull/13388)) +- Allow custom rocketchat username for crowd users and enable login via email/crowd_username ([#12981](https://github.com/RocketChat/Rocket.Chat/pull/12981) by [@steerben](https://github.com/steerben)) +- Add department field on find guest method ([#13491](https://github.com/RocketChat/Rocket.Chat/pull/13491)) +- KaTeX and Autolinker message rendering ([#11698](https://github.com/RocketChat/Rocket.Chat/pull/11698)) +- Update to MongoDB 4.0 in docker-compose file ([#13396](https://github.com/RocketChat/Rocket.Chat/pull/13396) by [@ngulden](https://github.com/ngulden)) +- Admin ui ([#13393](https://github.com/RocketChat/Rocket.Chat/pull/13393)) +- End to end tests ([#13401](https://github.com/RocketChat/Rocket.Chat/pull/13401)) +- Update deleteUser errors to be more semantic ([#12380](https://github.com/RocketChat/Rocket.Chat/pull/12380)) +- Line height on static content pages ([#11673](https://github.com/RocketChat/Rocket.Chat/pull/11673)) +- new icons ([#13289](https://github.com/RocketChat/Rocket.Chat/pull/13289)) +- Add permission to change other user profile avatar ([#13884](https://github.com/RocketChat/Rocket.Chat/pull/13884) by [@knrt10](https://github.com/knrt10)) +- UI of Permissions page ([#13732](https://github.com/RocketChat/Rocket.Chat/pull/13732) by [@fliptrail](https://github.com/fliptrail)) +- Use SessionId for credential token in SAML request ([#13791](https://github.com/RocketChat/Rocket.Chat/pull/13791) by [@MohammedEssehemy](https://github.com/MohammedEssehemy)) +- Include more information to help with bug reports and debugging ([#14047](https://github.com/RocketChat/Rocket.Chat/pull/14047)) +- New sidebar item badges, mention links, and ticks ([#14030](https://github.com/RocketChat/Rocket.Chat/pull/14030)) +- Remove setting to show a livechat is waiting ([#13992](https://github.com/RocketChat/Rocket.Chat/pull/13992)) +- Attachment download caching ([#14137](https://github.com/RocketChat/Rocket.Chat/pull/14137) by [@wreiske](https://github.com/wreiske)) +- Get avatar from oauth ([#14131](https://github.com/RocketChat/Rocket.Chat/pull/14131)) +- OAuth Role Sync ([#13761](https://github.com/RocketChat/Rocket.Chat/pull/13761) by [@hypery2k](https://github.com/hypery2k)) +- Update the Apps Engine version to v1.4.1 ([#14072](https://github.com/RocketChat/Rocket.Chat/pull/14072)) +- Replace livechat inquiry dialog with preview room ([#13986](https://github.com/RocketChat/Rocket.Chat/pull/13986)) + +### 🐛 Bug fixes + +- Opening a Livechat room from another agent ([#13951](https://github.com/RocketChat/Rocket.Chat/pull/13951)) +- Directory and Apps logs page ([#13938](https://github.com/RocketChat/Rocket.Chat/pull/13938)) +- Minor issues detected after testing the new Livechat client ([#13521](https://github.com/RocketChat/Rocket.Chat/pull/13521)) +- Display first message when taking Livechat inquiry ([#13896](https://github.com/RocketChat/Rocket.Chat/pull/13896)) +- Loading theme CSS on first server startup ([#13953](https://github.com/RocketChat/Rocket.Chat/pull/13953)) +- OTR dialog issue ([#13755](https://github.com/RocketChat/Rocket.Chat/pull/13755) by [@knrt10](https://github.com/knrt10)) +- Limit App’s HTTP calls to 500ms ([#13949](https://github.com/RocketChat/Rocket.Chat/pull/13949)) +- Read Receipt for Livechat Messages fixed ([#13832](https://github.com/RocketChat/Rocket.Chat/pull/13832) by [@knrt10](https://github.com/knrt10)) +- Avatar image being shrinked on autocomplete ([#13914](https://github.com/RocketChat/Rocket.Chat/pull/13914)) +- VIDEO/JITSI multiple calls before video call ([#13855](https://github.com/RocketChat/Rocket.Chat/pull/13855)) +- Some Safari bugs ([#13895](https://github.com/RocketChat/Rocket.Chat/pull/13895)) +- wrong width/height for tile_70 (mstile 70x70 (png)) ([#13851](https://github.com/RocketChat/Rocket.Chat/pull/13851) by [@ulf-f](https://github.com/ulf-f)) +- wrong importing of e2e ([#13863](https://github.com/RocketChat/Rocket.Chat/pull/13863)) +- Forwarded Livechat visitor name is not getting updated on the sidebar ([#13783](https://github.com/RocketChat/Rocket.Chat/pull/13783) by [@zolbayars](https://github.com/zolbayars)) +- Remove spaces in some i18n files ([#13801](https://github.com/RocketChat/Rocket.Chat/pull/13801)) +- Translation interpolations for many languages ([#13751](https://github.com/RocketChat/Rocket.Chat/pull/13751) by [@fliptrail](https://github.com/fliptrail)) +- Fixed grammatical error. ([#13559](https://github.com/RocketChat/Rocket.Chat/pull/13559) by [@gsunit](https://github.com/gsunit)) +- In home screen Rocket.Chat+ is dispalyed as Rocket.Chat ([#13784](https://github.com/RocketChat/Rocket.Chat/pull/13784) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- No new room created when conversation is closed ([#13753](https://github.com/RocketChat/Rocket.Chat/pull/13753) by [@knrt10](https://github.com/knrt10)) +- Loading user list from room messages ([#13769](https://github.com/RocketChat/Rocket.Chat/pull/13769)) +- User is unable to enter multiple emojis by clicking on the emoji icon ([#13744](https://github.com/RocketChat/Rocket.Chat/pull/13744) by [@Kailash0311](https://github.com/Kailash0311)) +- Audio message recording ([#13727](https://github.com/RocketChat/Rocket.Chat/pull/13727)) +- Remove Room info for Direct Messages (#9383) ([#12429](https://github.com/RocketChat/Rocket.Chat/pull/12429) by [@vinade](https://github.com/vinade)) +- WebRTC wasn't working duo to design and browser's APIs changes ([#13675](https://github.com/RocketChat/Rocket.Chat/pull/13675)) +- Adds Proper Language display name for many languages ([#13714](https://github.com/RocketChat/Rocket.Chat/pull/13714) by [@fliptrail](https://github.com/fliptrail)) +- Update bad-words to 3.0.2 ([#13705](https://github.com/RocketChat/Rocket.Chat/pull/13705) by [@trivoallan](https://github.com/trivoallan)) +- Changing Room name updates the webhook ([#13672](https://github.com/RocketChat/Rocket.Chat/pull/13672) by [@knrt10](https://github.com/knrt10)) +- Fix snap refresh hook ([#13702](https://github.com/RocketChat/Rocket.Chat/pull/13702)) +- Audio message recording issues ([#13486](https://github.com/RocketChat/Rocket.Chat/pull/13486)) +- Legal pages' style ([#13677](https://github.com/RocketChat/Rocket.Chat/pull/13677)) +- Stop livestream ([#13676](https://github.com/RocketChat/Rocket.Chat/pull/13676)) +- Avatar fonts for PNG and JPG ([#13681](https://github.com/RocketChat/Rocket.Chat/pull/13681)) +- Block User Icon ([#13630](https://github.com/RocketChat/Rocket.Chat/pull/13630) by [@knrt10](https://github.com/knrt10)) +- Corrects UI background of forced F2A Authentication ([#13670](https://github.com/RocketChat/Rocket.Chat/pull/13670) by [@fliptrail](https://github.com/fliptrail)) +- Race condition on the loading of Apps on the admin page ([#13587](https://github.com/RocketChat/Rocket.Chat/pull/13587)) +- Do not allow change avatars of another users without permission ([#13629](https://github.com/RocketChat/Rocket.Chat/pull/13629)) +- link of k8s deploy ([#13612](https://github.com/RocketChat/Rocket.Chat/pull/13612) by [@Mr-Linus](https://github.com/Mr-Linus)) +- Bugfix markdown Marked link new tab ([#13245](https://github.com/RocketChat/Rocket.Chat/pull/13245) by [@DeviaVir](https://github.com/DeviaVir)) +- Partially messaging formatting for bold letters ([#13599](https://github.com/RocketChat/Rocket.Chat/pull/13599) by [@knrt10](https://github.com/knrt10)) +- Change userId of rate limiter, change to logged user ([#13442](https://github.com/RocketChat/Rocket.Chat/pull/13442)) +- Add retries to docker-compose.yml, to wait for MongoDB to be ready ([#13199](https://github.com/RocketChat/Rocket.Chat/pull/13199) by [@tiangolo](https://github.com/tiangolo)) +- Non-latin room names and other slugifications ([#13467](https://github.com/RocketChat/Rocket.Chat/pull/13467)) +- Fixed rocketchat-oembed meta fragment pulling ([#13056](https://github.com/RocketChat/Rocket.Chat/pull/13056) by [@wreiske](https://github.com/wreiske)) +- Attachments without dates were showing December 31, 1970 ([#13428](https://github.com/RocketChat/Rocket.Chat/pull/13428) by [@wreiske](https://github.com/wreiske)) +- Restart required to apply changes in API Rate Limiter settings ([#13451](https://github.com/RocketChat/Rocket.Chat/pull/13451)) +- Ability to activate an app installed by zip even offline ([#13563](https://github.com/RocketChat/Rocket.Chat/pull/13563)) +- .bin extension added to attached file names ([#13468](https://github.com/RocketChat/Rocket.Chat/pull/13468)) +- Right arrows in default HTML content ([#13502](https://github.com/RocketChat/Rocket.Chat/pull/13502)) +- Typo in a referrer header in inject.js file ([#13469](https://github.com/RocketChat/Rocket.Chat/pull/13469) by [@algomaster99](https://github.com/algomaster99)) +- Fix issue cannot filter channels by name ([#12952](https://github.com/RocketChat/Rocket.Chat/pull/12952) by [@huydang284](https://github.com/huydang284)) +- mention-links not being always resolved ([#11745](https://github.com/RocketChat/Rocket.Chat/pull/11745)) +- allow user to logout before set username ([#13439](https://github.com/RocketChat/Rocket.Chat/pull/13439)) +- Error when recording data into the connection object ([#13553](https://github.com/RocketChat/Rocket.Chat/pull/13553)) +- Handle showing/hiding input in messageBox ([#13564](https://github.com/RocketChat/Rocket.Chat/pull/13564)) +- Fix wrong this scope in Notifications ([#13515](https://github.com/RocketChat/Rocket.Chat/pull/13515)) +- Get next Livechat agent endpoint ([#13485](https://github.com/RocketChat/Rocket.Chat/pull/13485)) +- Sidenav mouse hover was slow ([#13482](https://github.com/RocketChat/Rocket.Chat/pull/13482)) +- Emoji detection at line breaks ([#13447](https://github.com/RocketChat/Rocket.Chat/pull/13447) by [@savish28](https://github.com/savish28)) +- Small improvements on message box ([#13444](https://github.com/RocketChat/Rocket.Chat/pull/13444)) +- Fixing rooms find by type and name ([#11451](https://github.com/RocketChat/Rocket.Chat/pull/11451) by [@hmagarotto](https://github.com/hmagarotto)) +- linear-gradient background on safari ([#13363](https://github.com/RocketChat/Rocket.Chat/pull/13363)) +- Fixed text for "bulk-register-user" ([#11558](https://github.com/RocketChat/Rocket.Chat/pull/11558) by [@the4ndy](https://github.com/the4ndy)) +- Closing sidebar when room menu is clicked. ([#13842](https://github.com/RocketChat/Rocket.Chat/pull/13842) by [@Kailash0311](https://github.com/Kailash0311)) +- Check settings for name requirement before validating ([#14021](https://github.com/RocketChat/Rocket.Chat/pull/14021)) +- Links and upload paths when running in a subdir ([#13982](https://github.com/RocketChat/Rocket.Chat/pull/13982)) +- users.getPreferences when the user doesn't have any preferences ([#13532](https://github.com/RocketChat/Rocket.Chat/pull/13532) by [@thayannevls](https://github.com/thayannevls)) +- Real names were not displayed in the reactions (API/UI) ([#13495](https://github.com/RocketChat/Rocket.Chat/pull/13495)) +- Theme CSS loading in subdir env ([#14015](https://github.com/RocketChat/Rocket.Chat/pull/14015)) +- Fix rendering of links in the announcement modal ([#13250](https://github.com/RocketChat/Rocket.Chat/pull/13250) by [@supra08](https://github.com/supra08)) +- Add custom MIME types for *.ico extension ([#13969](https://github.com/RocketChat/Rocket.Chat/pull/13969)) +- Groups endpoints permission validations ([#13994](https://github.com/RocketChat/Rocket.Chat/pull/13994)) +- Focus on input when emoji picker box is open was not working ([#13981](https://github.com/RocketChat/Rocket.Chat/pull/13981)) +- Auto hide Livechat room from sidebar on close ([#13824](https://github.com/RocketChat/Rocket.Chat/pull/13824) by [@knrt10](https://github.com/knrt10)) +- Improve cloud section ([#13820](https://github.com/RocketChat/Rocket.Chat/pull/13820)) +- Wrong permalink when running in subdir ([#13746](https://github.com/RocketChat/Rocket.Chat/pull/13746) by [@ura14h](https://github.com/ura14h)) +- Change localStorage keys to work when server is running in a subdir ([#13968](https://github.com/RocketChat/Rocket.Chat/pull/13968)) +- SAML certificate settings don't follow a pattern ([#14179](https://github.com/RocketChat/Rocket.Chat/pull/14179)) +- Custom Oauth store refresh and id tokens with expiresIn ([#14121](https://github.com/RocketChat/Rocket.Chat/pull/14121) by [@ralfbecker](https://github.com/ralfbecker)) +- Apps converters delete fields on message attachments ([#14028](https://github.com/RocketChat/Rocket.Chat/pull/14028)) +- Custom Oauth login not working with accessToken ([#14113](https://github.com/RocketChat/Rocket.Chat/pull/14113) by [@knrt10](https://github.com/knrt10)) +- renderField template to correct short property usage ([#14148](https://github.com/RocketChat/Rocket.Chat/pull/14148)) +- Updating a message from apps if keep history is on ([#14129](https://github.com/RocketChat/Rocket.Chat/pull/14129)) +- Missing connection headers on Livechat REST API ([#14130](https://github.com/RocketChat/Rocket.Chat/pull/14130)) +- Receiving agent for new livechats from REST API ([#14103](https://github.com/RocketChat/Rocket.Chat/pull/14103)) +- Livechat user registration in another department ([#10695](https://github.com/RocketChat/Rocket.Chat/pull/10695)) +- Support for handling SAML LogoutRequest SLO ([#14074](https://github.com/RocketChat/Rocket.Chat/pull/14074)) +- Livechat office hours ([#14031](https://github.com/RocketChat/Rocket.Chat/pull/14031)) +- Auto-translate toggle not updating rendered messages ([#14262](https://github.com/RocketChat/Rocket.Chat/pull/14262)) +- Align burger menu in header with content matching room header ([#14265](https://github.com/RocketChat/Rocket.Chat/pull/14265)) +- Normalize TAPi18n language string on Livechat widget ([#14012](https://github.com/RocketChat/Rocket.Chat/pull/14012)) +- Autogrow not working properly for many message boxes ([#14163](https://github.com/RocketChat/Rocket.Chat/pull/14163)) +- Image attachment re-renders on message update ([#14207](https://github.com/RocketChat/Rocket.Chat/pull/14207) by [@Kailash0311](https://github.com/Kailash0311)) +- Sidenav does not open on some admin pages ([#14010](https://github.com/RocketChat/Rocket.Chat/pull/14010)) +- Empty result when getting badge count notification ([#14244](https://github.com/RocketChat/Rocket.Chat/pull/14244)) +- Obey audio notification preferences ([#14188](https://github.com/RocketChat/Rocket.Chat/pull/14188)) +- Slackbridge private channels ([#14273](https://github.com/RocketChat/Rocket.Chat/pull/14273) by [@nylen](https://github.com/nylen)) +- View All members button now not in direct room ([#14081](https://github.com/RocketChat/Rocket.Chat/pull/14081) by [@knrt10](https://github.com/knrt10)) + +
+🔍 Minor changes + +- Update eslint config ([#13966](https://github.com/RocketChat/Rocket.Chat/pull/13966)) +- Remove some bad references to messageBox ([#13954](https://github.com/RocketChat/Rocket.Chat/pull/13954)) +- LingoHub based on develop ([#13964](https://github.com/RocketChat/Rocket.Chat/pull/13964)) +- Update preview Dockerfile to use Stretch dependencies ([#13947](https://github.com/RocketChat/Rocket.Chat/pull/13947)) +- Small improvements to federation callbacks/hooks ([#13946](https://github.com/RocketChat/Rocket.Chat/pull/13946)) +- Improve: Support search and adding federated users through regular endpoints ([#13936](https://github.com/RocketChat/Rocket.Chat/pull/13936)) +- Remove bitcoin link in Readme.md since the link is broken ([#13935](https://github.com/RocketChat/Rocket.Chat/pull/13935) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- Fix missing dependencies on stretch CI image ([#13910](https://github.com/RocketChat/Rocket.Chat/pull/13910)) +- Remove some index.js files routing for server/client files ([#13772](https://github.com/RocketChat/Rocket.Chat/pull/13772)) +- Use CircleCI Debian Stretch images ([#13906](https://github.com/RocketChat/Rocket.Chat/pull/13906)) +- LingoHub based on develop ([#13891](https://github.com/RocketChat/Rocket.Chat/pull/13891)) +- User remove role dialog fixed ([#13874](https://github.com/RocketChat/Rocket.Chat/pull/13874) by [@bhardwajaditya](https://github.com/bhardwajaditya)) +- Rename Threads to Discussion ([#13782](https://github.com/RocketChat/Rocket.Chat/pull/13782)) +- [BUG] Icon Fixed for Knowledge base on Livechat ([#13806](https://github.com/RocketChat/Rocket.Chat/pull/13806) by [@knrt10](https://github.com/knrt10)) +- Add support to search for all users in directory ([#13803](https://github.com/RocketChat/Rocket.Chat/pull/13803)) +- LingoHub based on develop ([#13839](https://github.com/RocketChat/Rocket.Chat/pull/13839)) +- Remove unused style ([#13834](https://github.com/RocketChat/Rocket.Chat/pull/13834)) +- Remove unused files ([#13833](https://github.com/RocketChat/Rocket.Chat/pull/13833)) +- Lingohub sync and additional fixes ([#13825](https://github.com/RocketChat/Rocket.Chat/pull/13825)) +- Fix: addRoomAccessValidator method created for Threads ([#13789](https://github.com/RocketChat/Rocket.Chat/pull/13789)) +- Adds French translation of Personal Access Token ([#13779](https://github.com/RocketChat/Rocket.Chat/pull/13779) by [@ashwaniYDV](https://github.com/ashwaniYDV)) +- Remove Sandstorm support ([#13773](https://github.com/RocketChat/Rocket.Chat/pull/13773)) +- Removing (almost) every dynamic imports ([#13767](https://github.com/RocketChat/Rocket.Chat/pull/13767)) +- Regression: Threads styles improvement ([#13741](https://github.com/RocketChat/Rocket.Chat/pull/13741)) +- Convert imports to relative paths ([#13740](https://github.com/RocketChat/Rocket.Chat/pull/13740)) +- Regression: removed backup files ([#13729](https://github.com/RocketChat/Rocket.Chat/pull/13729)) +- Remove unused files ([#13725](https://github.com/RocketChat/Rocket.Chat/pull/13725)) +- Add Houston config ([#13707](https://github.com/RocketChat/Rocket.Chat/pull/13707)) +- Change the way to resolve DNS for Federation ([#13695](https://github.com/RocketChat/Rocket.Chat/pull/13695)) +- Update husky config ([#13687](https://github.com/RocketChat/Rocket.Chat/pull/13687)) +- Regression: Prune Threads ([#13683](https://github.com/RocketChat/Rocket.Chat/pull/13683)) +- Regression: Fix icon for DMs ([#13679](https://github.com/RocketChat/Rocket.Chat/pull/13679)) +- Regression: Add missing translations used in Apps pages ([#13674](https://github.com/RocketChat/Rocket.Chat/pull/13674)) +- Regression: User Discussions join message ([#13656](https://github.com/RocketChat/Rocket.Chat/pull/13656) by [@bhardwajaditya](https://github.com/bhardwajaditya)) +- Regression: Sidebar create new channel hover text ([#13658](https://github.com/RocketChat/Rocket.Chat/pull/13658) by [@bhardwajaditya](https://github.com/bhardwajaditya)) +- Regression: Fix embedded layout ([#13574](https://github.com/RocketChat/Rocket.Chat/pull/13574)) +- Improve: Send cloud token to Federation Hub ([#13651](https://github.com/RocketChat/Rocket.Chat/pull/13651)) +- Regression: Discussions - Invite users and DM ([#13646](https://github.com/RocketChat/Rocket.Chat/pull/13646)) +- LingoHub based on develop ([#13623](https://github.com/RocketChat/Rocket.Chat/pull/13623)) +- Force some words to translate in other languages ([#13367](https://github.com/RocketChat/Rocket.Chat/pull/13367) by [@soltanabadiyan](https://github.com/soltanabadiyan)) +- Fix wrong imports ([#13601](https://github.com/RocketChat/Rocket.Chat/pull/13601)) +- Fix: Some german translations ([#13299](https://github.com/RocketChat/Rocket.Chat/pull/13299) by [@soenkef](https://github.com/soenkef)) +- Add better positioning for tooltips on edges ([#13472](https://github.com/RocketChat/Rocket.Chat/pull/13472)) +- Fix: Mongo.setConnectionOptions was not being set correctly ([#13586](https://github.com/RocketChat/Rocket.Chat/pull/13586)) +- Regression: Missing settings import at `packages/rocketchat-livechat/server/methods/saveAppearance.js` ([#13573](https://github.com/RocketChat/Rocket.Chat/pull/13573)) +- Depack: Use mainModule for root files ([#13508](https://github.com/RocketChat/Rocket.Chat/pull/13508)) +- Regression: fix app pages styles ([#13567](https://github.com/RocketChat/Rocket.Chat/pull/13567)) +- Move mongo config away from cors package ([#13531](https://github.com/RocketChat/Rocket.Chat/pull/13531)) +- Regression: Add debounce on admin users search to avoid blocking by DDP Rate Limiter ([#13529](https://github.com/RocketChat/Rocket.Chat/pull/13529)) +- Remove Package references ([#13523](https://github.com/RocketChat/Rocket.Chat/pull/13523)) +- Remove Npm.depends and Npm.require except those that are inside package.js ([#13518](https://github.com/RocketChat/Rocket.Chat/pull/13518)) +- Update Meteor 1.8.0.2 ([#13519](https://github.com/RocketChat/Rocket.Chat/pull/13519)) +- Convert rc-nrr and slashcommands open to main module structure ([#13520](https://github.com/RocketChat/Rocket.Chat/pull/13520)) +- Regression: Fix wrong imports in rc-models ([#13516](https://github.com/RocketChat/Rocket.Chat/pull/13516)) +- Regression: Fix autolinker that was not parsing urls correctly ([#13497](https://github.com/RocketChat/Rocket.Chat/pull/13497)) +- Regression: Not updating subscriptions and not showing desktop notifcations ([#13509](https://github.com/RocketChat/Rocket.Chat/pull/13509)) +- Fix some imports from wrong packages, remove exports and files unused in rc-ui ([#13422](https://github.com/RocketChat/Rocket.Chat/pull/13422)) +- Remove functions from globals ([#13421](https://github.com/RocketChat/Rocket.Chat/pull/13421)) +- Remove unused files and code in rc-lib - step 3 ([#13420](https://github.com/RocketChat/Rocket.Chat/pull/13420)) +- Remove unused files in rc-lib - step 2 ([#13419](https://github.com/RocketChat/Rocket.Chat/pull/13419)) +- Remove unused files and code in rc-lib - step 1 ([#13416](https://github.com/RocketChat/Rocket.Chat/pull/13416)) +- Convert rocketchat-lib to main module structure ([#13415](https://github.com/RocketChat/Rocket.Chat/pull/13415)) +- Regression: Message box geolocation was throwing error ([#13496](https://github.com/RocketChat/Rocket.Chat/pull/13496)) +- Import missed functions to remove dependency of RC namespace ([#13414](https://github.com/RocketChat/Rocket.Chat/pull/13414)) +- Convert rocketchat-apps to main module structure ([#13409](https://github.com/RocketChat/Rocket.Chat/pull/13409)) +- Remove dependency of RC namespace in root server folder - step 6 ([#13405](https://github.com/RocketChat/Rocket.Chat/pull/13405)) +- Remove dependency of RC namespace in root server folder - step 5 ([#13402](https://github.com/RocketChat/Rocket.Chat/pull/13402)) +- Remove dependency of RC namespace in root server folder - step 4 ([#13400](https://github.com/RocketChat/Rocket.Chat/pull/13400)) +- Remove dependency of RC namespace in root server folder - step 3 ([#13398](https://github.com/RocketChat/Rocket.Chat/pull/13398)) +- Remove dependency of RC namespace in root server folder - step 2 ([#13397](https://github.com/RocketChat/Rocket.Chat/pull/13397)) +- Remove dependency of RC namespace in root server folder - step 1 ([#13390](https://github.com/RocketChat/Rocket.Chat/pull/13390)) +- Remove dependency of RC namespace in root client folder, imports/message-read-receipt and imports/personal-access-tokens ([#13389](https://github.com/RocketChat/Rocket.Chat/pull/13389)) +- Remove dependency of RC namespace in rc-integrations and importer-hipchat-enterprise ([#13386](https://github.com/RocketChat/Rocket.Chat/pull/13386)) +- Move rc-livechat server models to rc-models ([#13384](https://github.com/RocketChat/Rocket.Chat/pull/13384)) +- Remove dependency of RC namespace in rc-livechat/server/publications ([#13383](https://github.com/RocketChat/Rocket.Chat/pull/13383)) +- Remove dependency of RC namespace in rc-livechat/server/methods ([#13382](https://github.com/RocketChat/Rocket.Chat/pull/13382)) +- Remove dependency of RC namespace in rc-livechat/imports, lib, server/api, server/hooks and server/lib ([#13379](https://github.com/RocketChat/Rocket.Chat/pull/13379)) +- Remove LIvechat global variable from RC namespace ([#13378](https://github.com/RocketChat/Rocket.Chat/pull/13378)) +- Remove dependency of RC namespace in rc-livechat/server/models ([#13377](https://github.com/RocketChat/Rocket.Chat/pull/13377)) +- Remove dependency of RC namespace in livechat/client ([#13370](https://github.com/RocketChat/Rocket.Chat/pull/13370)) +- Remove dependency of RC namespace in rc-wordpress, chatpal-search and irc ([#13492](https://github.com/RocketChat/Rocket.Chat/pull/13492)) +- Remove dependency of RC namespace in rc-videobridge and webdav ([#13366](https://github.com/RocketChat/Rocket.Chat/pull/13366)) +- Remove dependency of RC namespace in rc-ui-master, ui-message- user-data-download and version-check ([#13365](https://github.com/RocketChat/Rocket.Chat/pull/13365)) +- Remove dependency of RC namespace in rc-ui-clean-history, ui-admin and ui-login ([#13362](https://github.com/RocketChat/Rocket.Chat/pull/13362)) +- Remove dependency of RC namespace in rc-ui, ui-account and ui-admin ([#13361](https://github.com/RocketChat/Rocket.Chat/pull/13361)) +- Remove dependency of RC namespace in rc-statistics and tokenpass ([#13359](https://github.com/RocketChat/Rocket.Chat/pull/13359)) +- Remove dependency of RC namespace in rc-smarsh-connector, sms and spotify ([#13358](https://github.com/RocketChat/Rocket.Chat/pull/13358)) +- Remove dependency of RC namespace in rc-slash-kick, leave, me, msg, mute, open, topic and unarchiveroom ([#13357](https://github.com/RocketChat/Rocket.Chat/pull/13357)) +- Remove dependency of RC namespace in rc-slash-archiveroom, create, help, hide, invite, inviteall and join ([#13356](https://github.com/RocketChat/Rocket.Chat/pull/13356)) +- Remove dependency of RC namespace in rc-setup-wizard, slackbridge and asciiarts ([#13348](https://github.com/RocketChat/Rocket.Chat/pull/13348)) +- Remove dependency of RC namespace in rc-reactions, retention-policy and search ([#13347](https://github.com/RocketChat/Rocket.Chat/pull/13347)) +- Remove dependency of RC namespace in rc-oembed and rc-otr ([#13345](https://github.com/RocketChat/Rocket.Chat/pull/13345)) +- Remove dependency of RC namespace in rc-oauth2-server and message-star ([#13344](https://github.com/RocketChat/Rocket.Chat/pull/13344)) +- Remove dependency of RC namespace in rc-message-pin and message-snippet ([#13343](https://github.com/RocketChat/Rocket.Chat/pull/13343)) +- Depackaging ([#13483](https://github.com/RocketChat/Rocket.Chat/pull/13483)) +- Merge master into develop & Set version to 1.0.0-develop ([#13435](https://github.com/RocketChat/Rocket.Chat/pull/13435) by [@TkTech](https://github.com/TkTech) & [@theundefined](https://github.com/theundefined)) +- Regression: Table admin pages ([#13411](https://github.com/RocketChat/Rocket.Chat/pull/13411)) +- Regression: Template error ([#13410](https://github.com/RocketChat/Rocket.Chat/pull/13410)) +- Removed old templates ([#13406](https://github.com/RocketChat/Rocket.Chat/pull/13406)) +- Add pagination to getUsersOfRoom ([#12834](https://github.com/RocketChat/Rocket.Chat/pull/12834)) +- OpenShift custom OAuth support ([#13925](https://github.com/RocketChat/Rocket.Chat/pull/13925) by [@bsharrow](https://github.com/bsharrow)) +- Settings: disable reset button ([#14026](https://github.com/RocketChat/Rocket.Chat/pull/14026)) +- Settings: hiding reset button for readonly fields ([#14025](https://github.com/RocketChat/Rocket.Chat/pull/14025)) +- Fix debug logging not being enabled by the setting ([#13979](https://github.com/RocketChat/Rocket.Chat/pull/13979)) +- Deprecate /api/v1/info in favor of /api/info ([#13798](https://github.com/RocketChat/Rocket.Chat/pull/13798)) +- Change dynamic dependency of FileUpload in Messages models ([#13776](https://github.com/RocketChat/Rocket.Chat/pull/13776)) +- Allow set env var METEOR_OPLOG_TOO_FAR_BEHIND ([#14017](https://github.com/RocketChat/Rocket.Chat/pull/14017)) +- Improve: Decrease padding for app buy modal ([#13984](https://github.com/RocketChat/Rocket.Chat/pull/13984)) +- Prioritize user-mentions badge ([#14057](https://github.com/RocketChat/Rocket.Chat/pull/14057)) +- Proper thread quote, clear message box on send, and other nice things to have ([#14049](https://github.com/RocketChat/Rocket.Chat/pull/14049)) +- Fix: Tests were not exiting RC instances ([#14054](https://github.com/RocketChat/Rocket.Chat/pull/14054)) +- Fix shield indentation ([#14048](https://github.com/RocketChat/Rocket.Chat/pull/14048)) +- Fix modal scroll ([#14052](https://github.com/RocketChat/Rocket.Chat/pull/14052)) +- Fix race condition of lastMessage set ([#14041](https://github.com/RocketChat/Rocket.Chat/pull/14041)) +- Fix room re-rendering ([#14044](https://github.com/RocketChat/Rocket.Chat/pull/14044)) +- Fix sending notifications to mentions on threads and discussion email sender ([#14043](https://github.com/RocketChat/Rocket.Chat/pull/14043)) +- Fix discussions issues after room deletion and translation actions not being shown ([#14018](https://github.com/RocketChat/Rocket.Chat/pull/14018)) +- Show discussion avatar ([#14053](https://github.com/RocketChat/Rocket.Chat/pull/14053)) +- Fix threads tests ([#14180](https://github.com/RocketChat/Rocket.Chat/pull/14180)) +- Prevent error for ldap login with invalid characters ([#14160](https://github.com/RocketChat/Rocket.Chat/pull/14160)) +- [REGRESSION] Messages sent by livechat's guests are losing sender info ([#14174](https://github.com/RocketChat/Rocket.Chat/pull/14174)) +- Faster CI build for PR ([#14171](https://github.com/RocketChat/Rocket.Chat/pull/14171)) +- Regression: Message box does not go back to initial state after sending a message ([#14161](https://github.com/RocketChat/Rocket.Chat/pull/14161)) +- Prevent error on normalize thread message for preview ([#14170](https://github.com/RocketChat/Rocket.Chat/pull/14170)) +- Update badges and mention links colors ([#14071](https://github.com/RocketChat/Rocket.Chat/pull/14071)) +- Smaller thread replies and system messages ([#14099](https://github.com/RocketChat/Rocket.Chat/pull/14099)) +- Regression: User autocomplete was not listing users from correct room ([#14125](https://github.com/RocketChat/Rocket.Chat/pull/14125)) +- Regression: Role creation and deletion error fixed ([#14097](https://github.com/RocketChat/Rocket.Chat/pull/14097) by [@knrt10](https://github.com/knrt10)) +- [Regression] Fix integrations message example ([#14111](https://github.com/RocketChat/Rocket.Chat/pull/14111)) +- Fix update apps capability of updating messages ([#14118](https://github.com/RocketChat/Rocket.Chat/pull/14118)) +- Fix: Skip thread notifications on message edit ([#14100](https://github.com/RocketChat/Rocket.Chat/pull/14100)) +- Fix: Remove message class `sequential` if `new-day` is present ([#14116](https://github.com/RocketChat/Rocket.Chat/pull/14116)) +- Fix top bar unread message counter ([#14102](https://github.com/RocketChat/Rocket.Chat/pull/14102)) +- LingoHub based on develop ([#14046](https://github.com/RocketChat/Rocket.Chat/pull/14046)) +- Fix sending message from action buttons in messages ([#14101](https://github.com/RocketChat/Rocket.Chat/pull/14101)) +- Fix: Error when version check endpoint was returning invalid data ([#14089](https://github.com/RocketChat/Rocket.Chat/pull/14089)) +- Wait port release to finish tests ([#14066](https://github.com/RocketChat/Rocket.Chat/pull/14066)) +- Fix threads rendering performance ([#14059](https://github.com/RocketChat/Rocket.Chat/pull/14059)) +- Unstuck observers every minute ([#14076](https://github.com/RocketChat/Rocket.Chat/pull/14076)) +- Fix messages losing thread titles on editing or reaction and improve message actions ([#14051](https://github.com/RocketChat/Rocket.Chat/pull/14051)) +- Improve message validation ([#14266](https://github.com/RocketChat/Rocket.Chat/pull/14266)) +- Added federation ping, loopback and dashboard ([#14007](https://github.com/RocketChat/Rocket.Chat/pull/14007)) +- Regression: Exception on notification when adding someone in room via mention ([#14251](https://github.com/RocketChat/Rocket.Chat/pull/14251)) +- Regression: fix grouping for reactive message ([#14246](https://github.com/RocketChat/Rocket.Chat/pull/14246)) +- Regression: Cursor position set to beginning when editing a message ([#14245](https://github.com/RocketChat/Rocket.Chat/pull/14245)) +- Regression: grouping messages on threads ([#14238](https://github.com/RocketChat/Rocket.Chat/pull/14238)) +- Regression: Remove border from unstyled message body ([#14235](https://github.com/RocketChat/Rocket.Chat/pull/14235)) +- Move LDAP Escape to login handler ([#14234](https://github.com/RocketChat/Rocket.Chat/pull/14234)) +- [Regression] Personal Access Token list fixed ([#14216](https://github.com/RocketChat/Rocket.Chat/pull/14216) by [@knrt10](https://github.com/knrt10)) +- ESLint: Add more import rules ([#14226](https://github.com/RocketChat/Rocket.Chat/pull/14226)) +- Regression: fix drop file ([#14225](https://github.com/RocketChat/Rocket.Chat/pull/14225)) +- Broken styles in Administration's contextual bar ([#14222](https://github.com/RocketChat/Rocket.Chat/pull/14222)) +- Regression: Broken UI for messages ([#14223](https://github.com/RocketChat/Rocket.Chat/pull/14223)) +- Exit process on unhandled rejection ([#14220](https://github.com/RocketChat/Rocket.Chat/pull/14220)) +- Unify mime-type package configuration ([#14217](https://github.com/RocketChat/Rocket.Chat/pull/14217)) +- Regression: Prevent startup errors for mentions parsing ([#14219](https://github.com/RocketChat/Rocket.Chat/pull/14219)) +- Regression: System messages styling ([#14189](https://github.com/RocketChat/Rocket.Chat/pull/14189)) +- Prevent click on reply thread to trigger flex tab closing ([#14215](https://github.com/RocketChat/Rocket.Chat/pull/14215)) +- created function to allow change default values, fix loading search users ([#14177](https://github.com/RocketChat/Rocket.Chat/pull/14177)) +- Use main message as thread tab title ([#14213](https://github.com/RocketChat/Rocket.Chat/pull/14213)) +- Use own logic to get thread infos via REST ([#14210](https://github.com/RocketChat/Rocket.Chat/pull/14210)) +- Regression: wrong expression at messageBox.actions.remove() ([#14192](https://github.com/RocketChat/Rocket.Chat/pull/14192)) +- Increment user counter on DMs ([#14185](https://github.com/RocketChat/Rocket.Chat/pull/14185)) +- [REGRESSION] Fix variable name references in message template ([#14184](https://github.com/RocketChat/Rocket.Chat/pull/14184)) +- Regression: Active room was not being marked ([#14276](https://github.com/RocketChat/Rocket.Chat/pull/14276)) +- Rename Cloud to Connectivity Services & split Apps in Apps and Marketplace ([#14211](https://github.com/RocketChat/Rocket.Chat/pull/14211)) +- LingoHub based on develop ([#14178](https://github.com/RocketChat/Rocket.Chat/pull/14178)) +- Regression: Discussions were not showing on Tab Bar ([#14050](https://github.com/RocketChat/Rocket.Chat/pull/14050) by [@knrt10](https://github.com/knrt10)) +- Force unstyling of blockquote under .message-body--unstyled ([#14274](https://github.com/RocketChat/Rocket.Chat/pull/14274)) +- Regression: Admin embedded layout ([#14229](https://github.com/RocketChat/Rocket.Chat/pull/14229)) +- New threads layout ([#14269](https://github.com/RocketChat/Rocket.Chat/pull/14269)) +- Improve: Marketplace auth inside Rocket.Chat instead of inside the iframe. ([#14258](https://github.com/RocketChat/Rocket.Chat/pull/14258)) +- [New] Reply privately to group messages ([#14150](https://github.com/RocketChat/Rocket.Chat/pull/14150) by [@bhardwajaditya](https://github.com/bhardwajaditya)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@DeviaVir](https://github.com/DeviaVir) +- [@Kailash0311](https://github.com/Kailash0311) +- [@MohammedEssehemy](https://github.com/MohammedEssehemy) +- [@Montel](https://github.com/Montel) +- [@Mr-Linus](https://github.com/Mr-Linus) +- [@Peym4n](https://github.com/Peym4n) +- [@TkTech](https://github.com/TkTech) +- [@algomaster99](https://github.com/algomaster99) +- [@ashwaniYDV](https://github.com/ashwaniYDV) +- [@bhardwajaditya](https://github.com/bhardwajaditya) +- [@bsharrow](https://github.com/bsharrow) +- [@fliptrail](https://github.com/fliptrail) +- [@gsunit](https://github.com/gsunit) +- [@hmagarotto](https://github.com/hmagarotto) +- [@huydang284](https://github.com/huydang284) +- [@hypery2k](https://github.com/hypery2k) +- [@jhnburke8](https://github.com/jhnburke8) +- [@john08burke](https://github.com/john08burke) +- [@kable-wilmoth](https://github.com/kable-wilmoth) +- [@knrt10](https://github.com/knrt10) +- [@localguru](https://github.com/localguru) +- [@mjovanovic0](https://github.com/mjovanovic0) +- [@ngulden](https://github.com/ngulden) +- [@nylen](https://github.com/nylen) +- [@pkolmann](https://github.com/pkolmann) +- [@ralfbecker](https://github.com/ralfbecker) +- [@rssilva](https://github.com/rssilva) +- [@savish28](https://github.com/savish28) +- [@soenkef](https://github.com/soenkef) +- [@soltanabadiyan](https://github.com/soltanabadiyan) +- [@steerben](https://github.com/steerben) +- [@supra08](https://github.com/supra08) +- [@thayannevls](https://github.com/thayannevls) +- [@the4ndy](https://github.com/the4ndy) +- [@theundefined](https://github.com/theundefined) +- [@tiangolo](https://github.com/tiangolo) +- [@trivoallan](https://github.com/trivoallan) +- [@ulf-f](https://github.com/ulf-f) +- [@ura14h](https://github.com/ura14h) +- [@vickyokrm](https://github.com/vickyokrm) +- [@vinade](https://github.com/vinade) +- [@wreiske](https://github.com/wreiske) +- [@xbolshe](https://github.com/xbolshe) +- [@zolbayars](https://github.com/zolbayars) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@LuluGO](https://github.com/LuluGO) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@alansikora](https://github.com/alansikora) +- [@d-gubert](https://github.com/d-gubert) +- [@engelgabriel](https://github.com/engelgabriel) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@graywolf336](https://github.com/graywolf336) +- [@marceloschmidt](https://github.com/marceloschmidt) +- [@mrsimpson](https://github.com/mrsimpson) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@timkinnane](https://github.com/timkinnane) + +# 0.74.3 +`2019-02-13 · 3 🚀 · 11 🐛 · 3 🔍 · 9 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🚀 Improvements + +- Open rooms quicker ([#13417](https://github.com/RocketChat/Rocket.Chat/pull/13417)) +- Allow configure Prometheus port per process via Environment Variable ([#13436](https://github.com/RocketChat/Rocket.Chat/pull/13436)) +- Add API option "permissionsRequired" ([#13430](https://github.com/RocketChat/Rocket.Chat/pull/13430)) + +### 🐛 Bug fixes + +- Invalid condition on getting next livechat agent over REST API endpoint ([#13360](https://github.com/RocketChat/Rocket.Chat/pull/13360)) +- "Test Desktop Notifications" not triggering a notification ([#13457](https://github.com/RocketChat/Rocket.Chat/pull/13457)) +- Translated and incorrect i18n variables ([#13463](https://github.com/RocketChat/Rocket.Chat/pull/13463) by [@leonboot](https://github.com/leonboot)) +- Properly escape custom emoji names for pattern matching ([#13408](https://github.com/RocketChat/Rocket.Chat/pull/13408)) +- Not translated emails ([#13452](https://github.com/RocketChat/Rocket.Chat/pull/13452)) +- XML-decryption module not found ([#13437](https://github.com/RocketChat/Rocket.Chat/pull/13437)) +- Update Russian localization ([#13244](https://github.com/RocketChat/Rocket.Chat/pull/13244) by [@BehindLoader](https://github.com/BehindLoader)) +- Several Problems on HipChat Importer ([#13336](https://github.com/RocketChat/Rocket.Chat/pull/13336)) +- Invalid push gateway configuration, requires the uniqueId ([#13423](https://github.com/RocketChat/Rocket.Chat/pull/13423)) +- Notify private settings changes even on public settings changed ([#13369](https://github.com/RocketChat/Rocket.Chat/pull/13369)) +- Misaligned upload progress bar "cancel" button ([#13407](https://github.com/RocketChat/Rocket.Chat/pull/13407)) + +
+🔍 Minor changes + +- Release 0.74.3 ([#13474](https://github.com/RocketChat/Rocket.Chat/pull/13474) by [@BehindLoader](https://github.com/BehindLoader) & [@leonboot](https://github.com/leonboot)) +- Room loading improvements ([#13471](https://github.com/RocketChat/Rocket.Chat/pull/13471)) +- Regression: Remove console.log on email translations ([#13456](https://github.com/RocketChat/Rocket.Chat/pull/13456)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@BehindLoader](https://github.com/BehindLoader) +- [@leonboot](https://github.com/leonboot) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@d-gubert](https://github.com/d-gubert) +- [@graywolf336](https://github.com/graywolf336) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.74.2 +`2019-02-05 · 1 🚀 · 3 🐛 · 4 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🚀 Improvements + +- Send `uniqueID` to all clients so Jitsi rooms can be created correctly ([#13342](https://github.com/RocketChat/Rocket.Chat/pull/13342)) + +### 🐛 Bug fixes + +- Rate Limiter was limiting communication between instances ([#13326](https://github.com/RocketChat/Rocket.Chat/pull/13326)) +- Setup wizard calling 'saveSetting' for each field/setting ([#13349](https://github.com/RocketChat/Rocket.Chat/pull/13349)) +- Pass token for cloud register ([#13350](https://github.com/RocketChat/Rocket.Chat/pull/13350)) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.74.1 +`2019-02-01 · 4 🎉 · 7 🐛 · 1 🔍 · 8 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🎉 New features + +- Limit all DDP/Websocket requests (configurable via admin panel) ([#13311](https://github.com/RocketChat/Rocket.Chat/pull/13311)) +- REST endpoint to forward livechat rooms ([#13308](https://github.com/RocketChat/Rocket.Chat/pull/13308)) +- Collect data for Monthly/Daily Active Users for a future dashboard ([#11525](https://github.com/RocketChat/Rocket.Chat/pull/11525)) +- Add parseUrls field to the apps message converter ([#13248](https://github.com/RocketChat/Rocket.Chat/pull/13248)) + +### 🐛 Bug fixes + +- Mobile view and re-enable E2E tests ([#13322](https://github.com/RocketChat/Rocket.Chat/pull/13322)) +- Hipchat Enterprise Importer not generating subscriptions ([#13293](https://github.com/RocketChat/Rocket.Chat/pull/13293)) +- Message updating by Apps ([#13294](https://github.com/RocketChat/Rocket.Chat/pull/13294)) +- REST endpoint for creating custom emojis ([#13306](https://github.com/RocketChat/Rocket.Chat/pull/13306)) +- Preview of image uploads were not working when apps framework is enable ([#13303](https://github.com/RocketChat/Rocket.Chat/pull/13303)) +- HipChat Enterprise importer fails when importing a large amount of messages (millions) ([#13221](https://github.com/RocketChat/Rocket.Chat/pull/13221)) +- Fix bug when user try recreate channel or group with same name and remove room from cache when user leaves room ([#12341](https://github.com/RocketChat/Rocket.Chat/pull/12341)) + +
+🔍 Minor changes + +- Fix: Missing export in cloud package ([#13282](https://github.com/RocketChat/Rocket.Chat/pull/13282)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@d-gubert](https://github.com/d-gubert) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.74.0 +`2019-01-28 · 10 🎉 · 11 🚀 · 17 🐛 · 38 🔍 · 24 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🎉 New features + +- SAML: Adds possibility to decrypt encrypted assertions ([#12153](https://github.com/RocketChat/Rocket.Chat/pull/12153) by [@gerbsen](https://github.com/gerbsen)) +- Add rate limiter to REST endpoints ([#11251](https://github.com/RocketChat/Rocket.Chat/pull/11251)) +- Added an option to disable email when activate and deactivate users ([#13183](https://github.com/RocketChat/Rocket.Chat/pull/13183)) +- Add create, update and delete endpoint for custom emojis ([#13160](https://github.com/RocketChat/Rocket.Chat/pull/13160)) +- Added endpoint to update timeout of the jitsi video conference ([#13167](https://github.com/RocketChat/Rocket.Chat/pull/13167)) +- Display total number of files and total upload size in admin ([#13184](https://github.com/RocketChat/Rocket.Chat/pull/13184)) +- Livechat GDPR compliance ([#12982](https://github.com/RocketChat/Rocket.Chat/pull/12982)) +- Added stream to notify when agent status change ([#13076](https://github.com/RocketChat/Rocket.Chat/pull/13076)) +- Add new Livechat REST endpoint to update the visitor's status ([#13108](https://github.com/RocketChat/Rocket.Chat/pull/13108)) +- Add Allow Methods directive to CORS ([#13073](https://github.com/RocketChat/Rocket.Chat/pull/13073)) + +### 🚀 Improvements + +- Dutch translations ([#12294](https://github.com/RocketChat/Rocket.Chat/pull/12294) by [@Jeroeny](https://github.com/Jeroeny)) +- Persian translations ([#13114](https://github.com/RocketChat/Rocket.Chat/pull/13114) by [@behnejad](https://github.com/behnejad)) +- Change the way the app detail screen shows support link when it's an email ([#13129](https://github.com/RocketChat/Rocket.Chat/pull/13129)) +- Process alerts from update checking ([#13194](https://github.com/RocketChat/Rocket.Chat/pull/13194)) +- Add "Apps Engine Version" to Administration > Info ([#13169](https://github.com/RocketChat/Rocket.Chat/pull/13169)) +- New Livechat statistics added to statistics collector ([#13168](https://github.com/RocketChat/Rocket.Chat/pull/13168)) +- Return room type field on Livechat findRoom method ([#13078](https://github.com/RocketChat/Rocket.Chat/pull/13078)) +- Return visitorEmails field on Livechat findGuest method ([#13097](https://github.com/RocketChat/Rocket.Chat/pull/13097)) +- Adds the "showConnecting" property to Livechat Config payload ([#13158](https://github.com/RocketChat/Rocket.Chat/pull/13158)) +- Adds history log for all Importers and improves HipChat import performance ([#13083](https://github.com/RocketChat/Rocket.Chat/pull/13083)) +- Inject metrics on callbacks ([#13266](https://github.com/RocketChat/Rocket.Chat/pull/13266)) + +### 🐛 Bug fixes + +- Few polish translating ([#13112](https://github.com/RocketChat/Rocket.Chat/pull/13112) by [@theundefined](https://github.com/theundefined)) +- Few polish translating ([#13112](https://github.com/RocketChat/Rocket.Chat/pull/13112) by [@theundefined](https://github.com/theundefined)) +- Update Message: Does not show edited when message was not edited. ([#13053](https://github.com/RocketChat/Rocket.Chat/pull/13053) by [@Kailash0311](https://github.com/Kailash0311)) +- Notifications for mentions not working on large rooms and don't emit desktop notifications for offline users ([#13067](https://github.com/RocketChat/Rocket.Chat/pull/13067)) +- Emoticons not displayed in room topic ([#12858](https://github.com/RocketChat/Rocket.Chat/pull/12858) by [@alexbartsch](https://github.com/alexbartsch)) +- REST API endpoint `users.getPersonalAccessTokens` error when user has no access tokens ([#13150](https://github.com/RocketChat/Rocket.Chat/pull/13150)) +- Remove unused code for Cordova ([#13188](https://github.com/RocketChat/Rocket.Chat/pull/13188)) +- Avatars with transparency were being converted to black ([#13181](https://github.com/RocketChat/Rocket.Chat/pull/13181)) +- REST api client base url on subdir ([#13180](https://github.com/RocketChat/Rocket.Chat/pull/13180)) +- Change webdav creation, due to changes in the npm lib after last update ([#13170](https://github.com/RocketChat/Rocket.Chat/pull/13170)) +- Invite command was not accpeting @ in username ([#12927](https://github.com/RocketChat/Rocket.Chat/pull/12927) by [@piotrkochan](https://github.com/piotrkochan)) +- Remove ES6 code from Livechat widget script ([#13105](https://github.com/RocketChat/Rocket.Chat/pull/13105)) +- User status on header and user info are not translated ([#13096](https://github.com/RocketChat/Rocket.Chat/pull/13096)) +- #11692 - Suppress error when drop collection in migration to suit to … ([#13091](https://github.com/RocketChat/Rocket.Chat/pull/13091) by [@Xuhao](https://github.com/Xuhao)) +- Change input type of e2e to password ([#13077](https://github.com/RocketChat/Rocket.Chat/pull/13077) by [@supra08](https://github.com/supra08)) +- LDAP login of new users overwriting `fname` from all subscriptions ([#13203](https://github.com/RocketChat/Rocket.Chat/pull/13203)) +- Snap upgrade add post-refresh hook ([#13153](https://github.com/RocketChat/Rocket.Chat/pull/13153)) + +
+🔍 Minor changes + +- Release 0.74.0 ([#13270](https://github.com/RocketChat/Rocket.Chat/pull/13270) by [@Xuhao](https://github.com/Xuhao) & [@supra08](https://github.com/supra08)) +- Regression: Fix message pinning ([#13213](https://github.com/RocketChat/Rocket.Chat/pull/13213) by [@TkTech](https://github.com/TkTech)) +- LingoHub based on develop ([#13201](https://github.com/RocketChat/Rocket.Chat/pull/13201)) +- Language: Edit typo "Обновлить" ([#13177](https://github.com/RocketChat/Rocket.Chat/pull/13177) by [@zpavlig](https://github.com/zpavlig)) +- Regression: Fix export AudioRecorder ([#13192](https://github.com/RocketChat/Rocket.Chat/pull/13192)) +- Remove dependency of RocketChat namespace and push-notifications ([#13137](https://github.com/RocketChat/Rocket.Chat/pull/13137)) +- Remove dependency of RocketChat namespace and custom-sounds ([#13136](https://github.com/RocketChat/Rocket.Chat/pull/13136)) +- Remove dependency of RocketChat namespace and logger ([#13135](https://github.com/RocketChat/Rocket.Chat/pull/13135)) +- Remove dependency between RocketChat namespace and migrations ([#13133](https://github.com/RocketChat/Rocket.Chat/pull/13133)) +- Convert rocketchat:ui to main module structure ([#13132](https://github.com/RocketChat/Rocket.Chat/pull/13132)) +- Remove dependency of RocketChat namespace inside rocketchat:ui ([#13131](https://github.com/RocketChat/Rocket.Chat/pull/13131)) +- Move some ui function to ui-utils ([#13123](https://github.com/RocketChat/Rocket.Chat/pull/13123)) +- Regression: fix upload permissions ([#13157](https://github.com/RocketChat/Rocket.Chat/pull/13157)) +- Move some function to utils ([#13122](https://github.com/RocketChat/Rocket.Chat/pull/13122)) +- Remove directly dependency between rocketchat:lib and emoji ([#13118](https://github.com/RocketChat/Rocket.Chat/pull/13118)) +- Convert rocketchat-webrtc to main module structure ([#13117](https://github.com/RocketChat/Rocket.Chat/pull/13117)) +- Remove directly dependency between lib and e2e ([#13115](https://github.com/RocketChat/Rocket.Chat/pull/13115)) +- Convert rocketchat-ui-master to main module structure ([#13107](https://github.com/RocketChat/Rocket.Chat/pull/13107)) +- Regression: fix rooms model's collection name ([#13146](https://github.com/RocketChat/Rocket.Chat/pull/13146)) +- Convert rocketchat-ui-sidenav to main module structure ([#13098](https://github.com/RocketChat/Rocket.Chat/pull/13098)) +- Convert rocketchat-file-upload to main module structure ([#13094](https://github.com/RocketChat/Rocket.Chat/pull/13094)) +- Remove dependency between lib and authz ([#13066](https://github.com/RocketChat/Rocket.Chat/pull/13066)) +- Globals/main module custom oauth ([#13037](https://github.com/RocketChat/Rocket.Chat/pull/13037)) +- Move UI Collections to rocketchat:models ([#13064](https://github.com/RocketChat/Rocket.Chat/pull/13064)) +- Rocketchat mailer ([#13036](https://github.com/RocketChat/Rocket.Chat/pull/13036)) +- Move rocketchat promises ([#13039](https://github.com/RocketChat/Rocket.Chat/pull/13039)) +- Globals/move rocketchat notifications ([#13035](https://github.com/RocketChat/Rocket.Chat/pull/13035)) +- Test only MongoDB with oplog versions 3.2 and 4.0 for PRs ([#13119](https://github.com/RocketChat/Rocket.Chat/pull/13119)) +- Move/create rocketchat callbacks ([#13034](https://github.com/RocketChat/Rocket.Chat/pull/13034)) +- Move/create rocketchat metrics ([#13032](https://github.com/RocketChat/Rocket.Chat/pull/13032)) +- Move rocketchat models ([#13027](https://github.com/RocketChat/Rocket.Chat/pull/13027)) +- Move rocketchat settings to specific package ([#13026](https://github.com/RocketChat/Rocket.Chat/pull/13026)) +- Remove incorrect pt-BR translation ([#13074](https://github.com/RocketChat/Rocket.Chat/pull/13074)) +- Merge master into develop & Set version to 0.74.0-develop ([#13050](https://github.com/RocketChat/Rocket.Chat/pull/13050) by [@ohmonster](https://github.com/ohmonster) & [@piotrkochan](https://github.com/piotrkochan)) +- Regression: Fix audio message upload ([#13224](https://github.com/RocketChat/Rocket.Chat/pull/13224)) +- Regression: Fix message pinning ([#13213](https://github.com/RocketChat/Rocket.Chat/pull/13213) by [@TkTech](https://github.com/TkTech)) +- Regression: Fix emoji search ([#13207](https://github.com/RocketChat/Rocket.Chat/pull/13207)) +- Change apps engine persistence bridge method to updateByAssociations ([#13239](https://github.com/RocketChat/Rocket.Chat/pull/13239)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@Jeroeny](https://github.com/Jeroeny) +- [@Kailash0311](https://github.com/Kailash0311) +- [@TkTech](https://github.com/TkTech) +- [@Xuhao](https://github.com/Xuhao) +- [@alexbartsch](https://github.com/alexbartsch) +- [@behnejad](https://github.com/behnejad) +- [@gerbsen](https://github.com/gerbsen) +- [@ohmonster](https://github.com/ohmonster) +- [@piotrkochan](https://github.com/piotrkochan) +- [@supra08](https://github.com/supra08) +- [@theundefined](https://github.com/theundefined) +- [@zpavlig](https://github.com/zpavlig) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@LuluGO](https://github.com/LuluGO) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@d-gubert](https://github.com/d-gubert) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@graywolf336](https://github.com/graywolf336) +- [@marceloschmidt](https://github.com/marceloschmidt) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.73.2 +`2019-01-07 · 1 🎉 · 1 🔍 · 3 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🎉 New features + +- Cloud Integration ([#13013](https://github.com/RocketChat/Rocket.Chat/pull/13013)) + +
+🔍 Minor changes + +- Release 0.73.2 ([#13086](https://github.com/RocketChat/Rocket.Chat/pull/13086)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@graywolf336](https://github.com/graywolf336) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.73.1 +`2018-12-28 · 1 🐛 · 3 🔍 · 2 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` +- MongoDB: `3.2, 3.4, 3.6, 4.0` + +### 🐛 Bug fixes + +- Default importer path ([#13045](https://github.com/RocketChat/Rocket.Chat/pull/13045)) + +
+🔍 Minor changes + +- Release 0.73.1 ([#13052](https://github.com/RocketChat/Rocket.Chat/pull/13052)) +- Execute tests with versions 3.2, 3.4, 3.6 and 4.0 of MongoDB ([#13049](https://github.com/RocketChat/Rocket.Chat/pull/13049)) +- Regression: Get room's members list not working on MongoDB 3.2 ([#13051](https://github.com/RocketChat/Rocket.Chat/pull/13051)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.73.0 +`2018-12-28 · 10 🎉 · 9 🚀 · 34 🐛 · 84 🔍 · 26 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.4` +- NPM: `6.4.1` + +### 🎉 New features + +- Create new permission.listAll endpoint to be able to use updatedSince parameter ([#12748](https://github.com/RocketChat/Rocket.Chat/pull/12748)) +- Mandatory 2fa for role ([#9748](https://github.com/RocketChat/Rocket.Chat/pull/9748) by [@karlprieb](https://github.com/karlprieb)) +- Add query parameter support to emoji-custom endpoint ([#12754](https://github.com/RocketChat/Rocket.Chat/pull/12754)) +- Added a link to contributing.md ([#12856](https://github.com/RocketChat/Rocket.Chat/pull/12856) by [@sanketsingh24](https://github.com/sanketsingh24)) +- Added chat.getDeletedMessages since specific date ([#13010](https://github.com/RocketChat/Rocket.Chat/pull/13010)) +- Download button for each file in fileslist ([#12874](https://github.com/RocketChat/Rocket.Chat/pull/12874) by [@alexbartsch](https://github.com/alexbartsch)) +- Syncloud deploy option ([#12867](https://github.com/RocketChat/Rocket.Chat/pull/12867) by [@cyberb](https://github.com/cyberb)) +- Config hooks for snap ([#12351](https://github.com/RocketChat/Rocket.Chat/pull/12351)) +- Livechat registration form message ([#12597](https://github.com/RocketChat/Rocket.Chat/pull/12597)) +- Include message type & id in push notification payload ([#12771](https://github.com/RocketChat/Rocket.Chat/pull/12771)) + +### 🚀 Improvements + +- Hipchat Enterprise Importer ([#12985](https://github.com/RocketChat/Rocket.Chat/pull/12985)) +- Add missing translation keys. ([#12722](https://github.com/RocketChat/Rocket.Chat/pull/12722) by [@ura14h](https://github.com/ura14h)) +- Accept Slash Commands via Action Buttons when `msg_in_chat_window: true` ([#13009](https://github.com/RocketChat/Rocket.Chat/pull/13009)) +- Allow transfer Livechats to online agents only ([#13008](https://github.com/RocketChat/Rocket.Chat/pull/13008)) +- Adding debugging instructions in README ([#12989](https://github.com/RocketChat/Rocket.Chat/pull/12989) by [@hypery2k](https://github.com/hypery2k)) +- Do not emit settings if there are no changes ([#12904](https://github.com/RocketChat/Rocket.Chat/pull/12904)) +- Returning an open room object in the Livechat config endpoint ([#12865](https://github.com/RocketChat/Rocket.Chat/pull/12865)) +- Use MongoBD aggregation to get users from a room ([#12566](https://github.com/RocketChat/Rocket.Chat/pull/12566)) +- Username suggestion logic ([#12779](https://github.com/RocketChat/Rocket.Chat/pull/12779)) + +### 🐛 Bug fixes + +- Avoiding links with highlighted words ([#12123](https://github.com/RocketChat/Rocket.Chat/pull/12123) by [@rssilva](https://github.com/rssilva)) +- Pin and unpin message were not checking permissions ([#12739](https://github.com/RocketChat/Rocket.Chat/pull/12739)) +- Fix users.setPreferences endpoint, set language correctly ([#12734](https://github.com/RocketChat/Rocket.Chat/pull/12734)) +- Fix set avatar http call, to avoid SSL errors ([#12790](https://github.com/RocketChat/Rocket.Chat/pull/12790)) +- Webdav integration account settings were being shown even when Webdav was disabled ([#12569](https://github.com/RocketChat/Rocket.Chat/pull/12569) by [@karakayasemi](https://github.com/karakayasemi)) +- Provide better Dutch translations 🇳🇱 ([#12792](https://github.com/RocketChat/Rocket.Chat/pull/12792) by [@mathysie](https://github.com/mathysie)) +- E2E`s password reaveal text is always `>%S` when language is zh ([#12795](https://github.com/RocketChat/Rocket.Chat/pull/12795) by [@lvyue](https://github.com/lvyue)) +- Nested Markdown blocks not parsed properly ([#12998](https://github.com/RocketChat/Rocket.Chat/pull/12998)) +- Change JSON to EJSON.parse query to support type Date ([#12706](https://github.com/RocketChat/Rocket.Chat/pull/12706)) +- Inherit font family in message user card ([#13004](https://github.com/RocketChat/Rocket.Chat/pull/13004)) +- Some deprecation issues for media recording ([#12948](https://github.com/RocketChat/Rocket.Chat/pull/12948)) +- Stop click event propagation on mention link or user card ([#12983](https://github.com/RocketChat/Rocket.Chat/pull/12983)) +- Change field checks in RocketChat.saveStreamingOptions ([#12973](https://github.com/RocketChat/Rocket.Chat/pull/12973)) +- Remove sharp's deprecation warnings on image upload ([#12980](https://github.com/RocketChat/Rocket.Chat/pull/12980)) +- Use web.browser.legacy bundle for Livechat script ([#12975](https://github.com/RocketChat/Rocket.Chat/pull/12975)) +- Revert Jitsi external API to an asset ([#12954](https://github.com/RocketChat/Rocket.Chat/pull/12954)) +- Exception in getSingleMessage ([#12970](https://github.com/RocketChat/Rocket.Chat/pull/12970) by [@tsukiRep](https://github.com/tsukiRep)) +- multiple rooms-changed ([#12940](https://github.com/RocketChat/Rocket.Chat/pull/12940)) +- Readable validation on the apps engine environment bridge ([#12994](https://github.com/RocketChat/Rocket.Chat/pull/12994)) +- Check for object falsehood before referencing properties in saveRoomSettings ([#12972](https://github.com/RocketChat/Rocket.Chat/pull/12972)) +- Spotlight being called while in background ([#12957](https://github.com/RocketChat/Rocket.Chat/pull/12957)) +- Padding for message box in embedded layout ([#12556](https://github.com/RocketChat/Rocket.Chat/pull/12556)) +- Crowd sync was being stopped when a user was not found ([#12930](https://github.com/RocketChat/Rocket.Chat/pull/12930) by [@piotrkochan](https://github.com/piotrkochan)) +- Some icons were missing ([#12913](https://github.com/RocketChat/Rocket.Chat/pull/12913)) +- User data download fails when a room has been deleted. ([#12829](https://github.com/RocketChat/Rocket.Chat/pull/12829)) +- CAS Login not working with renamed users ([#12860](https://github.com/RocketChat/Rocket.Chat/pull/12860)) +- Stream of my_message wasn't sending the room information ([#12914](https://github.com/RocketChat/Rocket.Chat/pull/12914)) +- cannot reset password ([#12903](https://github.com/RocketChat/Rocket.Chat/pull/12903)) +- Version check update notification ([#12905](https://github.com/RocketChat/Rocket.Chat/pull/12905)) +- Data Import not working ([#12866](https://github.com/RocketChat/Rocket.Chat/pull/12866)) +- Incorrect parameter name in Livechat stream ([#12851](https://github.com/RocketChat/Rocket.Chat/pull/12851)) +- Autotranslate icon on message action menu ([#12585](https://github.com/RocketChat/Rocket.Chat/pull/12585)) +- Google Cloud Storage storage provider ([#12843](https://github.com/RocketChat/Rocket.Chat/pull/12843)) +- Download files without extension wasn't possible ([#13033](https://github.com/RocketChat/Rocket.Chat/pull/13033)) + +
+🔍 Minor changes + +- LingoHub based on develop ([#13014](https://github.com/RocketChat/Rocket.Chat/pull/13014)) +- Move isFirefox and isChrome functions to rocketchat-utils ([#13011](https://github.com/RocketChat/Rocket.Chat/pull/13011)) +- Move tapi18n t and isRtl functions from ui to utils ([#13005](https://github.com/RocketChat/Rocket.Chat/pull/13005)) +- Remove /* globals */ wave 4 ([#12999](https://github.com/RocketChat/Rocket.Chat/pull/12999)) +- Remove /* globals */ wave 3 ([#12997](https://github.com/RocketChat/Rocket.Chat/pull/12997)) +- Convert rocketchat-logger to main module structure and remove Logger from eslintrc ([#12995](https://github.com/RocketChat/Rocket.Chat/pull/12995)) +- Remove /* globals */ wave 2 ([#12988](https://github.com/RocketChat/Rocket.Chat/pull/12988)) +- Remove /* globals */ from files wave-1 ([#12984](https://github.com/RocketChat/Rocket.Chat/pull/12984)) +- Move globals of test to a specific eslintrc file ([#12959](https://github.com/RocketChat/Rocket.Chat/pull/12959)) +- Remove global ServiceConfiguration ([#12960](https://github.com/RocketChat/Rocket.Chat/pull/12960)) +- Remove global toastr ([#12961](https://github.com/RocketChat/Rocket.Chat/pull/12961)) +- Convert rocketchat-livechat to main module structure ([#12942](https://github.com/RocketChat/Rocket.Chat/pull/12942)) +- changed maxRoomsOpen ([#12949](https://github.com/RocketChat/Rocket.Chat/pull/12949)) +- Revert imports of css, reAdd them to the addFiles function ([#12934](https://github.com/RocketChat/Rocket.Chat/pull/12934)) +- Convert rocketchat-theme to main module structure ([#12896](https://github.com/RocketChat/Rocket.Chat/pull/12896)) +- Convert rocketchat-katex to main module structure ([#12895](https://github.com/RocketChat/Rocket.Chat/pull/12895)) +- Convert rocketchat-webdav to main module structure ([#12886](https://github.com/RocketChat/Rocket.Chat/pull/12886)) +- Convert rocketchat-ui-message to main module structure ([#12871](https://github.com/RocketChat/Rocket.Chat/pull/12871)) +- Convert rocketchat-videobridge to main module structure ([#12881](https://github.com/RocketChat/Rocket.Chat/pull/12881)) +- Convert rocketchat-reactions to main module structure ([#12888](https://github.com/RocketChat/Rocket.Chat/pull/12888)) +- Convert rocketchat-wordpress to main module structure ([#12887](https://github.com/RocketChat/Rocket.Chat/pull/12887)) +- Fix: snap push from ci ([#12883](https://github.com/RocketChat/Rocket.Chat/pull/12883)) +- Convert rocketchat-version-check to main module structure ([#12879](https://github.com/RocketChat/Rocket.Chat/pull/12879)) +- Convert rocketchat-user-data-dowload to main module structure ([#12877](https://github.com/RocketChat/Rocket.Chat/pull/12877)) +- Convert rocketchat-ui-vrecord to main module structure ([#12875](https://github.com/RocketChat/Rocket.Chat/pull/12875)) +- Convert rocketchat-ui-login to main module structure ([#12861](https://github.com/RocketChat/Rocket.Chat/pull/12861)) +- Convert rocketchat-ui-flextab to main module structure ([#12859](https://github.com/RocketChat/Rocket.Chat/pull/12859)) +- German translation typo fix for Reacted_with ([#12761](https://github.com/RocketChat/Rocket.Chat/pull/12761) by [@localguru](https://github.com/localguru)) +- Convert rocketchat-ui-account to main module structure ([#12842](https://github.com/RocketChat/Rocket.Chat/pull/12842)) +- Convert rocketchat-ui-clean-history to main module structure ([#12846](https://github.com/RocketChat/Rocket.Chat/pull/12846)) +- Convert rocketchat-ui-admin to main module structure ([#12844](https://github.com/RocketChat/Rocket.Chat/pull/12844)) +- Convert rocketchat-tokenpass to main module structure ([#12838](https://github.com/RocketChat/Rocket.Chat/pull/12838)) +- Remove rocketchat-tutum package ([#12840](https://github.com/RocketChat/Rocket.Chat/pull/12840)) +- Convert rocketchat-tooltip to main module structure ([#12839](https://github.com/RocketChat/Rocket.Chat/pull/12839)) +- Convert rocketchat-token-login to main module structure ([#12837](https://github.com/RocketChat/Rocket.Chat/pull/12837)) +- Convert rocketchat-statistics to main module structure ([#12833](https://github.com/RocketChat/Rocket.Chat/pull/12833)) +- Convert rocketchat-spotify to main module structure ([#12832](https://github.com/RocketChat/Rocket.Chat/pull/12832)) +- Convert rocketchat-sms to main module structure ([#12831](https://github.com/RocketChat/Rocket.Chat/pull/12831)) +- Convert rocketchat-search to main module structure ([#12801](https://github.com/RocketChat/Rocket.Chat/pull/12801)) +- Convert rocketchat-message-pin to main module structure ([#12767](https://github.com/RocketChat/Rocket.Chat/pull/12767)) +- Convert rocketchat-message-star to main module structure ([#12770](https://github.com/RocketChat/Rocket.Chat/pull/12770)) +- Convert rocketchat-slashcommands-msg to main module structure ([#12823](https://github.com/RocketChat/Rocket.Chat/pull/12823)) +- Convert rocketchat-smarsh-connector to main module structure ([#12830](https://github.com/RocketChat/Rocket.Chat/pull/12830)) +- Convert rocketchat-slider to main module structure ([#12828](https://github.com/RocketChat/Rocket.Chat/pull/12828)) +- Convert rocketchat-slashcommands-unarchiveroom to main module structure ([#12827](https://github.com/RocketChat/Rocket.Chat/pull/12827)) +- Dependencies update ([#12624](https://github.com/RocketChat/Rocket.Chat/pull/12624)) +- Convert rocketchat-slashcommands-topic to main module structure ([#12826](https://github.com/RocketChat/Rocket.Chat/pull/12826)) +- Convert rocketchat-slashcommands-open to main module structure ([#12825](https://github.com/RocketChat/Rocket.Chat/pull/12825)) +- Convert rocketchat-slashcommands-mute to main module structure ([#12824](https://github.com/RocketChat/Rocket.Chat/pull/12824)) +- Convert rocketchat-slashcommands-me to main module structure ([#12822](https://github.com/RocketChat/Rocket.Chat/pull/12822)) +- Convert rocketchat-slashcommands-leave to main module structure ([#12821](https://github.com/RocketChat/Rocket.Chat/pull/12821)) +- Convert rocketchat-slashcommands-kick to main module structure ([#12817](https://github.com/RocketChat/Rocket.Chat/pull/12817)) +- Convert rocketchat-slashcommands-join to main module structure ([#12816](https://github.com/RocketChat/Rocket.Chat/pull/12816)) +- Convert rocketchat-slashcommands-inviteall to main module structure ([#12815](https://github.com/RocketChat/Rocket.Chat/pull/12815)) +- Convert rocketchat-slashcommands-invite to main module structure ([#12814](https://github.com/RocketChat/Rocket.Chat/pull/12814)) +- Convert rocketchat-slashcommands-hide to main module structure ([#12813](https://github.com/RocketChat/Rocket.Chat/pull/12813)) +- Convert rocketchat-slashcommands-help to main module structure ([#12812](https://github.com/RocketChat/Rocket.Chat/pull/12812)) +- Convert rocketchat-slashcommands-create to main module structure ([#12811](https://github.com/RocketChat/Rocket.Chat/pull/12811)) +- Convert rocketchat-slashcomands-archiveroom to main module structure ([#12810](https://github.com/RocketChat/Rocket.Chat/pull/12810)) +- Convert rocketchat-slashcommands-asciiarts to main module structure ([#12808](https://github.com/RocketChat/Rocket.Chat/pull/12808)) +- Convert rocketchat-slackbridge to main module structure ([#12807](https://github.com/RocketChat/Rocket.Chat/pull/12807)) +- Convert rocketchat-setup-wizard to main module structure ([#12806](https://github.com/RocketChat/Rocket.Chat/pull/12806)) +- Convert rocketchat-sandstorm to main module structure ([#12799](https://github.com/RocketChat/Rocket.Chat/pull/12799)) +- Convert rocketchat-oauth2-server-config to main module structure ([#12773](https://github.com/RocketChat/Rocket.Chat/pull/12773)) +- Convert rocketchat-message-snippet to main module structure ([#12768](https://github.com/RocketChat/Rocket.Chat/pull/12768)) +- Fix CI deploy job ([#12803](https://github.com/RocketChat/Rocket.Chat/pull/12803)) +- Convert rocketchat-retention-policy to main module structure ([#12797](https://github.com/RocketChat/Rocket.Chat/pull/12797)) +- Convert rocketchat-push-notifications to main module structure ([#12778](https://github.com/RocketChat/Rocket.Chat/pull/12778)) +- Convert rocketchat-otr to main module structure ([#12777](https://github.com/RocketChat/Rocket.Chat/pull/12777)) +- Convert rocketchat-oembed to main module structure ([#12775](https://github.com/RocketChat/Rocket.Chat/pull/12775)) +- Convert rocketchat-migrations to main-module structure ([#12772](https://github.com/RocketChat/Rocket.Chat/pull/12772)) +- Convert rocketchat-message-mark-as-unread to main module structure ([#12766](https://github.com/RocketChat/Rocket.Chat/pull/12766)) +- Remove conventional changelog cli, we are using our own cli now (Houston) ([#12798](https://github.com/RocketChat/Rocket.Chat/pull/12798)) +- Convert rocketchat-message-attachments to main module structure ([#12760](https://github.com/RocketChat/Rocket.Chat/pull/12760)) +- Convert rocketchat-message-action to main module structure ([#12759](https://github.com/RocketChat/Rocket.Chat/pull/12759)) +- Convert rocketchat-mentions-flextab to main module structure ([#12757](https://github.com/RocketChat/Rocket.Chat/pull/12757)) +- Convert rocketchat-mentions to main module structure ([#12756](https://github.com/RocketChat/Rocket.Chat/pull/12756)) +- Convert rocketchat-markdown to main module structure ([#12755](https://github.com/RocketChat/Rocket.Chat/pull/12755)) +- Convert rocketchat-mapview to main module structure ([#12701](https://github.com/RocketChat/Rocket.Chat/pull/12701)) +- Add check to make sure releases was updated ([#12791](https://github.com/RocketChat/Rocket.Chat/pull/12791)) +- Merge master into develop & Set version to 0.73.0-develop ([#12776](https://github.com/RocketChat/Rocket.Chat/pull/12776)) +- Change `chat.getDeletedMessages` to get messages after informed date and return only message's _id ([#13021](https://github.com/RocketChat/Rocket.Chat/pull/13021)) +- Improve Importer code quality ([#13020](https://github.com/RocketChat/Rocket.Chat/pull/13020)) +- Regression: List of custom emojis wasn't working ([#13031](https://github.com/RocketChat/Rocket.Chat/pull/13031)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@alexbartsch](https://github.com/alexbartsch) +- [@cyberb](https://github.com/cyberb) +- [@hypery2k](https://github.com/hypery2k) +- [@karakayasemi](https://github.com/karakayasemi) +- [@karlprieb](https://github.com/karlprieb) +- [@localguru](https://github.com/localguru) +- [@lvyue](https://github.com/lvyue) +- [@mathysie](https://github.com/mathysie) +- [@piotrkochan](https://github.com/piotrkochan) +- [@rssilva](https://github.com/rssilva) +- [@sanketsingh24](https://github.com/sanketsingh24) +- [@tsukiRep](https://github.com/tsukiRep) +- [@ura14h](https://github.com/ura14h) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@LuluGO](https://github.com/LuluGO) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@cardoso](https://github.com/cardoso) +- [@d-gubert](https://github.com/d-gubert) +- [@engelgabriel](https://github.com/engelgabriel) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@marceloschmidt](https://github.com/marceloschmidt) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.72.3 +`2018-12-12 · 1 🔍 · 5 👩‍💻👨‍💻` + +
+🔍 Minor changes + +- Release 0.72.3 ([#12932](https://github.com/RocketChat/Rocket.Chat/pull/12932) by [@piotrkochan](https://github.com/piotrkochan)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@piotrkochan](https://github.com/piotrkochan) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@ggazzo](https://github.com/ggazzo) +- [@rodrigok](https://github.com/rodrigok) +- [@tassoevan](https://github.com/tassoevan) + +# 0.72.2 +`2018-12-10 · 2 🐛 · 1 🔍 · 2 👩‍💻👨‍💻` + +### 🐛 Bug fixes + +- line-height for unread bar buttons (jump to first and mark as read) ([#12900](https://github.com/RocketChat/Rocket.Chat/pull/12900)) +- PDF view loading indicator ([#12882](https://github.com/RocketChat/Rocket.Chat/pull/12882)) + +
+🔍 Minor changes + +- Release 0.72.2 ([#12901](https://github.com/RocketChat/Rocket.Chat/pull/12901)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.72.1 +`2018-12-05 · 4 🐛 · 3 🔍 · 8 👩‍💻👨‍💻` + +### 🐛 Bug fixes + +- Change spread operator to Array.from for Edge browser ([#12818](https://github.com/RocketChat/Rocket.Chat/pull/12818) by [@ohmonster](https://github.com/ohmonster)) +- API users.info returns caller rooms and not requested user ones ([#12727](https://github.com/RocketChat/Rocket.Chat/pull/12727) by [@piotrkochan](https://github.com/piotrkochan)) +- Missing HipChat Enterprise Importer ([#12847](https://github.com/RocketChat/Rocket.Chat/pull/12847)) +- Emoji as avatar ([#12805](https://github.com/RocketChat/Rocket.Chat/pull/12805)) + +
+🔍 Minor changes + +- Release 0.72.1 ([#12850](https://github.com/RocketChat/Rocket.Chat/pull/12850) by [@ohmonster](https://github.com/ohmonster) & [@piotrkochan](https://github.com/piotrkochan)) +- Bump Apps-Engine version ([#12848](https://github.com/RocketChat/Rocket.Chat/pull/12848)) +- Change file order in rocketchat-cors ([#12804](https://github.com/RocketChat/Rocket.Chat/pull/12804)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@ohmonster](https://github.com/ohmonster) +- [@piotrkochan](https://github.com/piotrkochan) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@d-gubert](https://github.com/d-gubert) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.72.0 +`2018-11-28 · 1 ️️️⚠️ · 6 🎉 · 16 🚀 · 22 🐛 · 79 🔍 · 25 👩‍💻👨‍💻` + +### ⚠️ BREAKING CHANGES + +- Update to Meteor to 1.8 ([#12468](https://github.com/RocketChat/Rocket.Chat/pull/12468)) + +### 🎉 New features + +- Add permission to enable personal access token to specific roles ([#12309](https://github.com/RocketChat/Rocket.Chat/pull/12309)) +- Option to reset e2e key ([#12483](https://github.com/RocketChat/Rocket.Chat/pull/12483)) +- /api/v1/spotlight: return joinCodeRequired field for rooms ([#12651](https://github.com/RocketChat/Rocket.Chat/pull/12651)) +- New API Endpoints for the new version of JS SDK ([#12623](https://github.com/RocketChat/Rocket.Chat/pull/12623)) +- Setting to configure robots.txt content ([#12547](https://github.com/RocketChat/Rocket.Chat/pull/12547)) +- Make Livechat's widget draggable ([#12378](https://github.com/RocketChat/Rocket.Chat/pull/12378)) + +### 🚀 Improvements + +- Improve unreads and unreadsFrom response, prevent it to be equal null ([#12563](https://github.com/RocketChat/Rocket.Chat/pull/12563)) +- Add rooms property in user object, if the user has the permission, with rooms roles ([#12105](https://github.com/RocketChat/Rocket.Chat/pull/12105)) +- border-radius to use --border-radius ([#12675](https://github.com/RocketChat/Rocket.Chat/pull/12675)) +- Update the 'keyboard shortcuts' documentation ([#12564](https://github.com/RocketChat/Rocket.Chat/pull/12564) by [@nicolasbock](https://github.com/nicolasbock)) +- Add new acceptable header for Livechat REST requests ([#12561](https://github.com/RocketChat/Rocket.Chat/pull/12561)) +- Atlassian Crowd settings and option to sync user data ([#12616](https://github.com/RocketChat/Rocket.Chat/pull/12616)) +- CircleCI to use MongoDB 4.0 for testing ([#12618](https://github.com/RocketChat/Rocket.Chat/pull/12618)) +- Japanese translations ([#12382](https://github.com/RocketChat/Rocket.Chat/pull/12382) by [@ura14h](https://github.com/ura14h)) +- Add CTRL modifier for keyboard shortcut ([#12525](https://github.com/RocketChat/Rocket.Chat/pull/12525) by [@nicolasbock](https://github.com/nicolasbock)) +- Ignore non-existent Livechat custom fields on Livechat API ([#12522](https://github.com/RocketChat/Rocket.Chat/pull/12522)) +- Emoji search on messageBox behaving like emojiPicker's search (#9607) ([#12452](https://github.com/RocketChat/Rocket.Chat/pull/12452) by [@vinade](https://github.com/vinade)) +- German translations ([#12471](https://github.com/RocketChat/Rocket.Chat/pull/12471)) +- Limit the number of typing users shown (#8722) ([#12400](https://github.com/RocketChat/Rocket.Chat/pull/12400) by [@vinade](https://github.com/vinade)) +- Allow apps to update persistence by association ([#12714](https://github.com/RocketChat/Rocket.Chat/pull/12714)) +- Add more methods to deal with rooms via Rocket.Chat.Apps ([#12680](https://github.com/RocketChat/Rocket.Chat/pull/12680)) +- Better query for finding subscriptions that need a new E2E Key ([#12692](https://github.com/RocketChat/Rocket.Chat/pull/12692)) + +### 🐛 Bug fixes + +- Fixed Anonymous Registration ([#12633](https://github.com/RocketChat/Rocket.Chat/pull/12633) by [@wreiske](https://github.com/wreiske)) +- high cpu usage ~ svg icon ([#12677](https://github.com/RocketChat/Rocket.Chat/pull/12677) by [@ph1p](https://github.com/ph1p)) +- Fix favico error ([#12643](https://github.com/RocketChat/Rocket.Chat/pull/12643)) +- Condition to not render PDF preview ([#12632](https://github.com/RocketChat/Rocket.Chat/pull/12632)) +- Admin styles ([#12614](https://github.com/RocketChat/Rocket.Chat/pull/12614)) +- Admin styles ([#12602](https://github.com/RocketChat/Rocket.Chat/pull/12602)) +- Change registration message when user need to confirm email ([#9336](https://github.com/RocketChat/Rocket.Chat/pull/9336) by [@karlprieb](https://github.com/karlprieb)) +- Import missed file in rocketchat-authorization ([#12570](https://github.com/RocketChat/Rocket.Chat/pull/12570)) +- Prevent subscriptions and calls to rooms events that the user is not participating ([#12558](https://github.com/RocketChat/Rocket.Chat/pull/12558)) +- Wrong test case for `users.setAvatar` endpoint ([#12539](https://github.com/RocketChat/Rocket.Chat/pull/12539)) +- Spotlight method being called multiple times ([#12536](https://github.com/RocketChat/Rocket.Chat/pull/12536)) +- German translation for for API_EmbedIgnoredHosts label ([#12518](https://github.com/RocketChat/Rocket.Chat/pull/12518) by [@mbrodala](https://github.com/mbrodala)) +- Handle all events for enter key in message box ([#12507](https://github.com/RocketChat/Rocket.Chat/pull/12507)) +- Fix wrong parameter in chat.delete endpoint and add some test cases ([#12408](https://github.com/RocketChat/Rocket.Chat/pull/12408)) +- Manage own integrations permissions check ([#12397](https://github.com/RocketChat/Rocket.Chat/pull/12397)) +- stream room-changed ([#12411](https://github.com/RocketChat/Rocket.Chat/pull/12411)) +- Emoji picker is not in viewport on small screens ([#12457](https://github.com/RocketChat/Rocket.Chat/pull/12457) by [@ramrami](https://github.com/ramrami)) +- `Disabled` word translation to Spanish ([#12406](https://github.com/RocketChat/Rocket.Chat/pull/12406) by [@Ismaw34](https://github.com/Ismaw34)) +- `Disabled` word translation to Chinese ([#12260](https://github.com/RocketChat/Rocket.Chat/pull/12260) by [@AndreamApp](https://github.com/AndreamApp)) +- Correct roomName value in Mail Messages (#12363) ([#12453](https://github.com/RocketChat/Rocket.Chat/pull/12453) by [@vinade](https://github.com/vinade)) +- Update caret position on insert a new line in message box ([#12713](https://github.com/RocketChat/Rocket.Chat/pull/12713)) +- DE translation for idle-time-limit ([#12637](https://github.com/RocketChat/Rocket.Chat/pull/12637) by [@pfuender](https://github.com/pfuender)) + +
+🔍 Minor changes + +- LingoHub based on develop ([#12684](https://github.com/RocketChat/Rocket.Chat/pull/12684)) +- Convert rocketchat-mail-messages to main module structure ([#12682](https://github.com/RocketChat/Rocket.Chat/pull/12682)) +- Convert rocketchat-livestream to main module structure ([#12679](https://github.com/RocketChat/Rocket.Chat/pull/12679)) +- Added "npm install" to quick start for developers ([#12374](https://github.com/RocketChat/Rocket.Chat/pull/12374) by [@wreiske](https://github.com/wreiske)) +- Convert rocketchat-ldap to main module structure ([#12678](https://github.com/RocketChat/Rocket.Chat/pull/12678)) +- Convert rocketchat-issuelinks to main module structure ([#12674](https://github.com/RocketChat/Rocket.Chat/pull/12674)) +- Convert rocketchat-integrations to main module structure ([#12670](https://github.com/RocketChat/Rocket.Chat/pull/12670)) +- Convert rocketchat-irc to main module structure ([#12672](https://github.com/RocketChat/Rocket.Chat/pull/12672)) +- Convert rocketchat-internal-hubot to main module structure ([#12671](https://github.com/RocketChat/Rocket.Chat/pull/12671)) +- Convert rocketchat-importer-hipchat-enterprise to main module structure ([#12665](https://github.com/RocketChat/Rocket.Chat/pull/12665)) +- Convert rocketchat-importer-slack-users to main module structure ([#12669](https://github.com/RocketChat/Rocket.Chat/pull/12669)) +- Convert rocketchat-importer-slack to main module structure ([#12666](https://github.com/RocketChat/Rocket.Chat/pull/12666)) +- Convert rocketchat-iframe-login to main module structure ([#12661](https://github.com/RocketChat/Rocket.Chat/pull/12661)) +- Convert rocketchat-importer to main module structure ([#12662](https://github.com/RocketChat/Rocket.Chat/pull/12662)) +- Convert rocketchat-importer-csv to main module structure ([#12663](https://github.com/RocketChat/Rocket.Chat/pull/12663)) +- Convert rocketchat-importer-hipchat to main module structure ([#12664](https://github.com/RocketChat/Rocket.Chat/pull/12664)) +- Convert rocketchat-highlight-words to main module structure ([#12659](https://github.com/RocketChat/Rocket.Chat/pull/12659)) +- Convert rocketchat-grant to main module structure ([#12657](https://github.com/RocketChat/Rocket.Chat/pull/12657)) +- Convert rocketchat-graphql to main module structure ([#12658](https://github.com/RocketChat/Rocket.Chat/pull/12658)) +- Convert rocketchat-google-vision to main module structure ([#12649](https://github.com/RocketChat/Rocket.Chat/pull/12649)) +- Removed RocketChatFile from globals ([#12650](https://github.com/RocketChat/Rocket.Chat/pull/12650)) +- Added imports for global variables in rocketchat-google-natural-language package ([#12647](https://github.com/RocketChat/Rocket.Chat/pull/12647)) +- Convert rocketchat-gitlab to main module structure ([#12646](https://github.com/RocketChat/Rocket.Chat/pull/12646)) +- Convert rocketchat-file to main module structure ([#12644](https://github.com/RocketChat/Rocket.Chat/pull/12644)) +- Convert rocketchat-github-enterprise to main module structure ([#12642](https://github.com/RocketChat/Rocket.Chat/pull/12642)) +- Fix: Add email dependency in package.js ([#12645](https://github.com/RocketChat/Rocket.Chat/pull/12645)) +- Convert rocketchat-custom-sounds to main module structure ([#12599](https://github.com/RocketChat/Rocket.Chat/pull/12599)) +- Fix crowd error with import of SyncedCron ([#12641](https://github.com/RocketChat/Rocket.Chat/pull/12641)) +- Convert emoji-emojione to main module structure ([#12605](https://github.com/RocketChat/Rocket.Chat/pull/12605)) +- Convert rocketchat-favico to main module structure ([#12607](https://github.com/RocketChat/Rocket.Chat/pull/12607)) +- Convert rocketchat-emoji-custom to main module structure ([#12604](https://github.com/RocketChat/Rocket.Chat/pull/12604)) +- Convert rocketchat-error-handler to main module structure ([#12606](https://github.com/RocketChat/Rocket.Chat/pull/12606)) +- Convert rocketchat-drupal to main module structure ([#12601](https://github.com/RocketChat/Rocket.Chat/pull/12601)) +- Convert rocketchat-crowd to main module structure ([#12596](https://github.com/RocketChat/Rocket.Chat/pull/12596)) +- Convert rocketchat-emoji to main module structure ([#12603](https://github.com/RocketChat/Rocket.Chat/pull/12603)) +- Fix users.setAvatar endpoint tests and logic ([#12625](https://github.com/RocketChat/Rocket.Chat/pull/12625)) +- [DOCS] Remove Cordova links, include F-Droid download button and few other adjustments ([#12583](https://github.com/RocketChat/Rocket.Chat/pull/12583) by [@rafaelks](https://github.com/rafaelks)) +- Convert rocketchat-dolphin to main module structure ([#12600](https://github.com/RocketChat/Rocket.Chat/pull/12600)) +- Convert rocketchat-channel-settings to main module structure ([#12594](https://github.com/RocketChat/Rocket.Chat/pull/12594)) +- Convert rocketchat-cors to main module structure ([#12595](https://github.com/RocketChat/Rocket.Chat/pull/12595)) +- Convert rocketchat-autotranslate to main module structure ([#12530](https://github.com/RocketChat/Rocket.Chat/pull/12530)) +- Convert rocketchat-channel-settings-mail-messages to main module structure ([#12537](https://github.com/RocketChat/Rocket.Chat/pull/12537)) +- Convert rocketchat-colors to main module structure ([#12538](https://github.com/RocketChat/Rocket.Chat/pull/12538)) +- Convert rocketchat-cas to main module structure ([#12532](https://github.com/RocketChat/Rocket.Chat/pull/12532)) +- Convert rocketchat-bot-helpers to main module structure ([#12531](https://github.com/RocketChat/Rocket.Chat/pull/12531)) +- Convert rocketchat-autolinker to main module structure ([#12529](https://github.com/RocketChat/Rocket.Chat/pull/12529)) +- Convert rocketchat-authorization to main module structure ([#12523](https://github.com/RocketChat/Rocket.Chat/pull/12523)) +- Fix CSS import order ([#12524](https://github.com/RocketChat/Rocket.Chat/pull/12524)) +- Remove template for feature requests as issues ([#12426](https://github.com/RocketChat/Rocket.Chat/pull/12426)) +- Fix punctuation, spelling, and grammar ([#12451](https://github.com/RocketChat/Rocket.Chat/pull/12451) by [@imronras](https://github.com/imronras)) +- Convert rocketchat-assets to main module structure ([#12521](https://github.com/RocketChat/Rocket.Chat/pull/12521)) +- Convert rocketchat-api to main module structure ([#12510](https://github.com/RocketChat/Rocket.Chat/pull/12510)) +- Convert rocketchat-analytics to main module structure ([#12506](https://github.com/RocketChat/Rocket.Chat/pull/12506)) +- Convert rocketchat-action-links to main module structure ([#12503](https://github.com/RocketChat/Rocket.Chat/pull/12503)) +- Convert rocketchat-2fa to main module structure ([#12501](https://github.com/RocketChat/Rocket.Chat/pull/12501)) +- Convert meteor-timesync to main module structure ([#12495](https://github.com/RocketChat/Rocket.Chat/pull/12495)) +- Convert meteor-autocomplete package to main module structure ([#12491](https://github.com/RocketChat/Rocket.Chat/pull/12491)) +- Convert meteor-accounts-saml to main module structure ([#12486](https://github.com/RocketChat/Rocket.Chat/pull/12486)) +- Convert chatpal search package to modular structure ([#12485](https://github.com/RocketChat/Rocket.Chat/pull/12485)) +- Removal of TAPi18n and TAPi18next global variables ([#12467](https://github.com/RocketChat/Rocket.Chat/pull/12467)) +- Removal of Template, Blaze, BlazeLayout, FlowRouter, DDPRateLimiter, Session, UAParser, Promise, Reload and CryptoJS global variables ([#12433](https://github.com/RocketChat/Rocket.Chat/pull/12433)) +- Removal of Match, check, moment, Tracker and Mongo global variables ([#12410](https://github.com/RocketChat/Rocket.Chat/pull/12410)) +- Removal of EJSON, Accounts, Email, HTTP, Random, ReactiveDict, ReactiveVar, SHA256 and WebApp global variables ([#12377](https://github.com/RocketChat/Rocket.Chat/pull/12377)) +- Removal of Meteor global variable ([#12371](https://github.com/RocketChat/Rocket.Chat/pull/12371)) +- Fix ES translation ([#12509](https://github.com/RocketChat/Rocket.Chat/pull/12509)) +- LingoHub based on develop ([#12470](https://github.com/RocketChat/Rocket.Chat/pull/12470)) +- Update npm dependencies ([#12465](https://github.com/RocketChat/Rocket.Chat/pull/12465)) +- Fix: Developers not being able to debug root files in VSCode ([#12440](https://github.com/RocketChat/Rocket.Chat/pull/12440)) +- Merge master into develop & Set version to 0.72.0-develop ([#12460](https://github.com/RocketChat/Rocket.Chat/pull/12460)) +- Fix some Ukrainian translations ([#12712](https://github.com/RocketChat/Rocket.Chat/pull/12712) by [@zdumitru](https://github.com/zdumitru)) +- Improve: Add missing translation keys. ([#12708](https://github.com/RocketChat/Rocket.Chat/pull/12708) by [@ura14h](https://github.com/ura14h)) +- Bump Apps Engine to 1.3.0 ([#12705](https://github.com/RocketChat/Rocket.Chat/pull/12705)) +- Fix: Exception when registering a user with gravatar ([#12699](https://github.com/RocketChat/Rocket.Chat/pull/12699)) +- Fix: Fix tests by increasing window size ([#12707](https://github.com/RocketChat/Rocket.Chat/pull/12707)) +- Update Apps Engine to 1.3.1 ([#12741](https://github.com/RocketChat/Rocket.Chat/pull/12741)) +- Regression: Expand Administration sections by toggling section title ([#12736](https://github.com/RocketChat/Rocket.Chat/pull/12736)) +- Regression: Fix Safari detection in PDF previewing ([#12737](https://github.com/RocketChat/Rocket.Chat/pull/12737)) +- Regression: Account pages layout ([#12735](https://github.com/RocketChat/Rocket.Chat/pull/12735)) +- Regression: Inherit font-family for message box ([#12729](https://github.com/RocketChat/Rocket.Chat/pull/12729)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@AndreamApp](https://github.com/AndreamApp) +- [@Ismaw34](https://github.com/Ismaw34) +- [@imronras](https://github.com/imronras) +- [@karlprieb](https://github.com/karlprieb) +- [@mbrodala](https://github.com/mbrodala) +- [@nicolasbock](https://github.com/nicolasbock) +- [@pfuender](https://github.com/pfuender) +- [@ph1p](https://github.com/ph1p) +- [@rafaelks](https://github.com/rafaelks) +- [@ramrami](https://github.com/ramrami) +- [@ura14h](https://github.com/ura14h) +- [@vinade](https://github.com/vinade) +- [@wreiske](https://github.com/wreiske) +- [@zdumitru](https://github.com/zdumitru) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@cardoso](https://github.com/cardoso) +- [@engelgabriel](https://github.com/engelgabriel) +- [@ggazzo](https://github.com/ggazzo) +- [@marceloschmidt](https://github.com/marceloschmidt) +- [@mrsimpson](https://github.com/mrsimpson) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.71.2 +`2018-12-10` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +# 0.71.1 +`2018-10-31 · 1 🐛 · 1 🔍 · 1 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### 🐛 Bug fixes + +- Email sending with GDPR user data ([#12487](https://github.com/RocketChat/Rocket.Chat/pull/12487)) + +
+🔍 Minor changes + +- Release 0.71.1 ([#12499](https://github.com/RocketChat/Rocket.Chat/pull/12499)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.71.0 +`2018-10-27 · 2 ️️️⚠️ · 5 🎉 · 5 🚀 · 21 🐛 · 8 🔍 · 20 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### ⚠️ BREAKING CHANGES + +- Update `lastMessage` rooms property and convert the "starred" property, to the same format ([#12266](https://github.com/RocketChat/Rocket.Chat/pull/12266)) +- Add expiration to API login tokens and fix duplicate login tokens created by LDAP ([#12186](https://github.com/RocketChat/Rocket.Chat/pull/12186)) + +### 🎉 New features + +- Add delete channel mutation to GraphQL API ([#11860](https://github.com/RocketChat/Rocket.Chat/pull/11860)) +- sidenav size on large screens ([#12372](https://github.com/RocketChat/Rocket.Chat/pull/12372)) +- Ability to disable user presence monitor ([#12353](https://github.com/RocketChat/Rocket.Chat/pull/12353)) +- PDF message attachment preview (client side rendering) ([#10519](https://github.com/RocketChat/Rocket.Chat/pull/10519) by [@kb0304](https://github.com/kb0304)) +- Add "help wanted" section to Readme ([#12432](https://github.com/RocketChat/Rocket.Chat/pull/12432) by [@isabellarussell](https://github.com/isabellarussell)) + +### 🚀 Improvements + +- Livechat room closure endpoints ([#12360](https://github.com/RocketChat/Rocket.Chat/pull/12360)) +- Set Livechat department before register guest ([#12161](https://github.com/RocketChat/Rocket.Chat/pull/12161)) +- Add missing livechat i18n keys ([#12330](https://github.com/RocketChat/Rocket.Chat/pull/12330) by [@MarcosEllys](https://github.com/MarcosEllys)) +- Avoid unnecessary calls to Meteor.user() on client ([#11212](https://github.com/RocketChat/Rocket.Chat/pull/11212)) +- Allow the imports to accept any file type ([#12425](https://github.com/RocketChat/Rocket.Chat/pull/12425)) + +### 🐛 Bug fixes + +- Add image dimensions to attachment even when no reorientation is required ([#11521](https://github.com/RocketChat/Rocket.Chat/pull/11521)) +- iframe login token not checked ([#12158](https://github.com/RocketChat/Rocket.Chat/pull/12158) by [@nimetu](https://github.com/nimetu)) +- REST `users.setAvatar` endpoint wasn't allowing update the avatar of other users even with correct permissions ([#11431](https://github.com/RocketChat/Rocket.Chat/pull/11431)) +- Slack importer: image previews not showing ([#11875](https://github.com/RocketChat/Rocket.Chat/pull/11875) by [@madguy02](https://github.com/madguy02)) +- Edit room name with uppercase letters ([#12235](https://github.com/RocketChat/Rocket.Chat/pull/12235) by [@nikeee](https://github.com/nikeee)) +- Custom OAuth Configuration can't be removed ([#12256](https://github.com/RocketChat/Rocket.Chat/pull/12256)) +- Remove e2e from users endpoint responses ([#12344](https://github.com/RocketChat/Rocket.Chat/pull/12344)) +- email api TAPi18n is undefined ([#12373](https://github.com/RocketChat/Rocket.Chat/pull/12373)) +- Blockstack errors in IE 11 ([#12338](https://github.com/RocketChat/Rocket.Chat/pull/12338)) +- avatar?_dc=undefined ([#12365](https://github.com/RocketChat/Rocket.Chat/pull/12365)) +- users.register endpoint to not create an user if username already being used ([#12297](https://github.com/RocketChat/Rocket.Chat/pull/12297)) +- Date range check on livechat analytics ([#12345](https://github.com/RocketChat/Rocket.Chat/pull/12345) by [@teresy](https://github.com/teresy)) +- Cast env var setting to int based on option type ([#12194](https://github.com/RocketChat/Rocket.Chat/pull/12194) by [@crazy-max](https://github.com/crazy-max)) +- Links in home layout ([#12355](https://github.com/RocketChat/Rocket.Chat/pull/12355) by [@upiksaleh](https://github.com/upiksaleh)) +- Last message not updating after message delete if show deleted status is on ([#12350](https://github.com/RocketChat/Rocket.Chat/pull/12350)) +- Invalid destructuring on Livechat API endpoint ([#12354](https://github.com/RocketChat/Rocket.Chat/pull/12354)) +- E2E: Decrypting UTF-8 encoded messages ([#12398](https://github.com/RocketChat/Rocket.Chat/pull/12398) by [@pmmaga](https://github.com/pmmaga)) +- Ignore errors when creating image preview for uploads ([#12424](https://github.com/RocketChat/Rocket.Chat/pull/12424)) +- Attachment actions not being collapsable ([#12436](https://github.com/RocketChat/Rocket.Chat/pull/12436)) +- Attachment timestamp from and to Apps system not working ([#12445](https://github.com/RocketChat/Rocket.Chat/pull/12445)) +- Apps not being able to state how the action buttons are aligned ([#12391](https://github.com/RocketChat/Rocket.Chat/pull/12391)) + +
+🔍 Minor changes + +- Fix: wrong saveUser permission validations ([#12384](https://github.com/RocketChat/Rocket.Chat/pull/12384)) +- Regression: do not render pdf preview on safari <= 12 ([#12375](https://github.com/RocketChat/Rocket.Chat/pull/12375)) +- Improve: Drop database between running tests on CI ([#12358](https://github.com/RocketChat/Rocket.Chat/pull/12358)) +- Fix: update check on err.details ([#12346](https://github.com/RocketChat/Rocket.Chat/pull/12346) by [@teresy](https://github.com/teresy)) +- Update Apps Framework to version 1.2.1 ([#12442](https://github.com/RocketChat/Rocket.Chat/pull/12442)) +- Regression: Change `starred` message property from object to array ([#12405](https://github.com/RocketChat/Rocket.Chat/pull/12405)) +- Apps: Room’s usernames was not working ([#12409](https://github.com/RocketChat/Rocket.Chat/pull/12409)) +- Regression: Fix email headers not being used ([#12392](https://github.com/RocketChat/Rocket.Chat/pull/12392)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@MarcosEllys](https://github.com/MarcosEllys) +- [@crazy-max](https://github.com/crazy-max) +- [@isabellarussell](https://github.com/isabellarussell) +- [@kb0304](https://github.com/kb0304) +- [@madguy02](https://github.com/madguy02) +- [@nikeee](https://github.com/nikeee) +- [@nimetu](https://github.com/nimetu) +- [@pmmaga](https://github.com/pmmaga) +- [@teresy](https://github.com/teresy) +- [@upiksaleh](https://github.com/upiksaleh) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@Sing-Li](https://github.com/Sing-Li) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@graywolf336](https://github.com/graywolf336) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) + +# 0.70.5 +`2018-12-10 · 1 🐛 · 1 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### 🐛 Bug fixes + +- Reset password email ([#12898](https://github.com/RocketChat/Rocket.Chat/pull/12898)) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.70.4 +`2018-10-09 · 1 🐛 · 2 🔍 · 1 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### 🐛 Bug fixes + +- Modal confirm on enter ([#12283](https://github.com/RocketChat/Rocket.Chat/pull/12283)) + +
+🔍 Minor changes + +- Release 0.70.4 ([#12299](https://github.com/RocketChat/Rocket.Chat/pull/12299)) +- Fix: Add wizard opt-in fields ([#12298](https://github.com/RocketChat/Rocket.Chat/pull/12298)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.70.3 +`2018-10-08 · 1 🐛 · 2 🔍 · 2 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### 🐛 Bug fixes + +- E2E alert shows up when encryption is disabled ([#12272](https://github.com/RocketChat/Rocket.Chat/pull/12272)) + +
+🔍 Minor changes + +- Release 0.70.3 ([#12281](https://github.com/RocketChat/Rocket.Chat/pull/12281)) +- Release 0.70.2 ([#12276](https://github.com/RocketChat/Rocket.Chat/pull/12276)) + +
+ +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@sampaiodiego](https://github.com/sampaiodiego) + +# 0.70.1 +`2018-10-05 · 8 🐛 · 5 🔍 · 11 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### 🐛 Bug fixes + +- E2E data not cleared on logout ([#12254](https://github.com/RocketChat/Rocket.Chat/pull/12254)) +- E2E password request not closing after entering password ([#12232](https://github.com/RocketChat/Rocket.Chat/pull/12232)) +- Message editing was duplicating reply quotes ([#12263](https://github.com/RocketChat/Rocket.Chat/pull/12263)) +- Livechat integration with RDStation ([#12257](https://github.com/RocketChat/Rocket.Chat/pull/12257)) +- Livechat triggers being registered twice after setting department via API ([#12255](https://github.com/RocketChat/Rocket.Chat/pull/12255) by [@edzluhan](https://github.com/edzluhan)) +- Livechat CRM integration running when disabled ([#12242](https://github.com/RocketChat/Rocket.Chat/pull/12242)) +- Emails' logo and links ([#12241](https://github.com/RocketChat/Rocket.Chat/pull/12241)) +- Set default action for Setup Wizard form submit ([#12240](https://github.com/RocketChat/Rocket.Chat/pull/12240)) + +
+🔍 Minor changes + +- Release 0.70.1 ([#12270](https://github.com/RocketChat/Rocket.Chat/pull/12270) by [@edzluhan](https://github.com/edzluhan)) +- Merge master into develop & Set version to 0.71.0-develop ([#12264](https://github.com/RocketChat/Rocket.Chat/pull/12264) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) +- Regression: fix modal submit ([#12233](https://github.com/RocketChat/Rocket.Chat/pull/12233)) +- Add reetp to the issues' bot whitelist ([#12227](https://github.com/RocketChat/Rocket.Chat/pull/12227)) +- Fix: Remove semver satisfies from Apps details that is already done my marketplace ([#12268](https://github.com/RocketChat/Rocket.Chat/pull/12268)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@edzluhan](https://github.com/edzluhan) +- [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@cardoso](https://github.com/cardoso) +- [@ggazzo](https://github.com/ggazzo) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@theorenck](https://github.com/theorenck) +- [@timkinnane](https://github.com/timkinnane) + +# 0.70.0 +`2018-09-28 · 2 ️️️⚠️ · 18 🎉 · 3 🚀 · 31 🐛 · 21 🔍 · 33 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### ⚠️ BREAKING CHANGES + +- Update the default port of the Prometheus exporter ([#11351](https://github.com/RocketChat/Rocket.Chat/pull/11351) by [@thaiphv](https://github.com/thaiphv)) +- [IMPROVE] New emails design ([#12009](https://github.com/RocketChat/Rocket.Chat/pull/12009)) + +### 🎉 New features + +- Allow multiple subcommands in MIGRATION_VERSION env variable ([#11184](https://github.com/RocketChat/Rocket.Chat/pull/11184) by [@arch119](https://github.com/arch119)) +- Support for end to end encryption ([#10094](https://github.com/RocketChat/Rocket.Chat/pull/10094) by [@mrinaldhar](https://github.com/mrinaldhar)) +- Livechat Analytics and Reports ([#11238](https://github.com/RocketChat/Rocket.Chat/pull/11238) by [@pkgodara](https://github.com/pkgodara)) +- Apps: Add handlers for message updates ([#11993](https://github.com/RocketChat/Rocket.Chat/pull/11993)) +- Livechat notifications on new incoming inquiries for guest-pool ([#10588](https://github.com/RocketChat/Rocket.Chat/pull/10588)) +- Customizable default directory view ([#11965](https://github.com/RocketChat/Rocket.Chat/pull/11965) by [@ohmonster](https://github.com/ohmonster)) +- Blockstack as decentralized auth provider ([#12047](https://github.com/RocketChat/Rocket.Chat/pull/12047)) +- Livechat REST endpoints ([#11900](https://github.com/RocketChat/Rocket.Chat/pull/11900)) +- REST endpoints to get moderators from groups and channels ([#11909](https://github.com/RocketChat/Rocket.Chat/pull/11909)) +- User preference for 24- or 12-hour clock ([#11169](https://github.com/RocketChat/Rocket.Chat/pull/11169) by [@vynmera](https://github.com/vynmera)) +- REST endpoint to set groups' announcement ([#11905](https://github.com/RocketChat/Rocket.Chat/pull/11905)) +- Livechat trigger option to run only once ([#12068](https://github.com/RocketChat/Rocket.Chat/pull/12068) by [@edzluhan](https://github.com/edzluhan)) +- REST endpoints to create roles and assign roles to users ([#11855](https://github.com/RocketChat/Rocket.Chat/pull/11855) by [@aferreira44](https://github.com/aferreira44)) +- Informal German translations ([#9984](https://github.com/RocketChat/Rocket.Chat/pull/9984)) +- Apps: API provider ([#11938](https://github.com/RocketChat/Rocket.Chat/pull/11938)) +- Apps are enabled by default now ([#12189](https://github.com/RocketChat/Rocket.Chat/pull/12189)) +- Add Livechat Analytics permission ([#12184](https://github.com/RocketChat/Rocket.Chat/pull/12184)) +- WebDAV Integration (User file provider) ([#11679](https://github.com/RocketChat/Rocket.Chat/pull/11679) by [@karakayasemi](https://github.com/karakayasemi)) + +### 🚀 Improvements + +- Cache livechat get agent trigger call ([#12083](https://github.com/RocketChat/Rocket.Chat/pull/12083)) +- BigBlueButton joinViaHtml5 and video icon on sidebar ([#12107](https://github.com/RocketChat/Rocket.Chat/pull/12107)) +- Use eslint-config package ([#12044](https://github.com/RocketChat/Rocket.Chat/pull/12044)) + +### 🐛 Bug fixes + +- Livechat agent joining on pick from guest pool ([#12097](https://github.com/RocketChat/Rocket.Chat/pull/12097)) +- Apps: Add missing reactions and actions properties to app message object ([#11780](https://github.com/RocketChat/Rocket.Chat/pull/11780)) +- Broken slack compatible webhook ([#11742](https://github.com/RocketChat/Rocket.Chat/pull/11742)) +- Changing Mentions.userMentionRegex pattern to include
tag ([#12043](https://github.com/RocketChat/Rocket.Chat/pull/12043) by [@rssilva](https://github.com/rssilva)) +- Double output of message actions ([#11902](https://github.com/RocketChat/Rocket.Chat/pull/11902)) +- Login error message not obvious if user not activated ([#11785](https://github.com/RocketChat/Rocket.Chat/pull/11785) by [@crazy-max](https://github.com/crazy-max)) +- Adding scroll bar to read receipts modal ([#11919](https://github.com/RocketChat/Rocket.Chat/pull/11919) by [@rssilva](https://github.com/rssilva)) +- Fixing translation on 'yesterday' word when calling timeAgo function ([#11946](https://github.com/RocketChat/Rocket.Chat/pull/11946) by [@rssilva](https://github.com/rssilva)) +- Fixing spacement between tags and words on some labels ([#12018](https://github.com/RocketChat/Rocket.Chat/pull/12018) by [@rssilva](https://github.com/rssilva)) +- video message recording, issue #11651 ([#12031](https://github.com/RocketChat/Rocket.Chat/pull/12031) by [@flaviogrossi](https://github.com/flaviogrossi)) +- Prevent form submission in Files List search ([#11999](https://github.com/RocketChat/Rocket.Chat/pull/11999)) +- Re-add the eye-off icon ([#12079](https://github.com/RocketChat/Rocket.Chat/pull/12079) by [@MIKI785](https://github.com/MIKI785)) +- Internal error when cross-origin with CORS is disabled ([#11953](https://github.com/RocketChat/Rocket.Chat/pull/11953)) +- Message reaction in GraphQL API ([#11967](https://github.com/RocketChat/Rocket.Chat/pull/11967)) +- Direct messages leaking into logs ([#11863](https://github.com/RocketChat/Rocket.Chat/pull/11863)) +- Wrong build path in install.sh ([#11879](https://github.com/RocketChat/Rocket.Chat/pull/11879)) +- Permission check on joinRoom for private room ([#11857](https://github.com/RocketChat/Rocket.Chat/pull/11857)) +- Close popover on shortcuts and writing ([#11562](https://github.com/RocketChat/Rocket.Chat/pull/11562)) +- Typo in a configuration key for SlackBridge excluded bot names ([#11872](https://github.com/RocketChat/Rocket.Chat/pull/11872) by [@TobiasKappe](https://github.com/TobiasKappe)) +- Real Name on Direct Messages ([#12154](https://github.com/RocketChat/Rocket.Chat/pull/12154)) +- Position of popover component on mobile ([#12038](https://github.com/RocketChat/Rocket.Chat/pull/12038)) +- Duplicate email and auto-join on mentions ([#12168](https://github.com/RocketChat/Rocket.Chat/pull/12168)) +- Horizontal scroll on user info tab ([#12102](https://github.com/RocketChat/Rocket.Chat/pull/12102) by [@rssilva](https://github.com/rssilva)) +- Markdown ampersand escape on links ([#12140](https://github.com/RocketChat/Rocket.Chat/pull/12140) by [@rssilva](https://github.com/rssilva)) +- Saving user preferences ([#12170](https://github.com/RocketChat/Rocket.Chat/pull/12170)) +- Apps being able to see hidden settings ([#12159](https://github.com/RocketChat/Rocket.Chat/pull/12159)) +- Allow user with "bulk-register-user" permission to send invitations ([#12112](https://github.com/RocketChat/Rocket.Chat/pull/12112)) +- IRC Federation no longer working ([#11906](https://github.com/RocketChat/Rocket.Chat/pull/11906)) +- Files list missing from popover menu when owner of room ([#11565](https://github.com/RocketChat/Rocket.Chat/pull/11565)) +- Not able to set per-channel retention policies if no global policy is set for this channel type ([#11927](https://github.com/RocketChat/Rocket.Chat/pull/11927) by [@vynmera](https://github.com/vynmera)) +- app engine verbose log typo ([#12126](https://github.com/RocketChat/Rocket.Chat/pull/12126) by [@williamriancho](https://github.com/williamriancho)) + +
+🔍 Minor changes + +- Release 0.69.2 ([#12026](https://github.com/RocketChat/Rocket.Chat/pull/12026) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) +- LingoHub based on develop ([#11936](https://github.com/RocketChat/Rocket.Chat/pull/11936)) +- Better organize package.json ([#12115](https://github.com/RocketChat/Rocket.Chat/pull/12115)) +- Fix using wrong variable ([#12114](https://github.com/RocketChat/Rocket.Chat/pull/12114)) +- Fix the style lint ([#11991](https://github.com/RocketChat/Rocket.Chat/pull/11991)) +- Merge master into develop & Set version to 0.70.0-develop ([#11921](https://github.com/RocketChat/Rocket.Chat/pull/11921) by [@c0dzilla](https://github.com/c0dzilla) & [@rndmh3ro](https://github.com/rndmh3ro) & [@ubarsaiyan](https://github.com/ubarsaiyan) & [@vynmera](https://github.com/vynmera)) +- Release 0.69.2 ([#12026](https://github.com/RocketChat/Rocket.Chat/pull/12026) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) +- Regression: fix message box autogrow ([#12138](https://github.com/RocketChat/Rocket.Chat/pull/12138)) +- Regression: Modal height ([#12122](https://github.com/RocketChat/Rocket.Chat/pull/12122)) +- Fix: Change wording on e2e to make a little more clear ([#12124](https://github.com/RocketChat/Rocket.Chat/pull/12124)) +- Improve: Moved the e2e password request to an alert instead of a popup ([#12172](https://github.com/RocketChat/Rocket.Chat/pull/12172)) +- New: Option to change E2E key ([#12169](https://github.com/RocketChat/Rocket.Chat/pull/12169)) +- Improve: Decrypt last message ([#12173](https://github.com/RocketChat/Rocket.Chat/pull/12173)) +- Fix: e2e password visible on always-on alert message. ([#12139](https://github.com/RocketChat/Rocket.Chat/pull/12139)) +- Improve: Expose apps enable setting at `General > Apps` ([#12196](https://github.com/RocketChat/Rocket.Chat/pull/12196)) +- Fix: Message changing order when been edited with apps enabled ([#12188](https://github.com/RocketChat/Rocket.Chat/pull/12188)) +- Improve: E2E setting description and alert ([#12191](https://github.com/RocketChat/Rocket.Chat/pull/12191)) +- Improve: Do not start E2E Encryption when accessing admin as embedded ([#12192](https://github.com/RocketChat/Rocket.Chat/pull/12192)) +- Fix: Add e2e doc to the alert ([#12187](https://github.com/RocketChat/Rocket.Chat/pull/12187)) +- Improve: Switch e2e doc to target _blank ([#12195](https://github.com/RocketChat/Rocket.Chat/pull/12195)) +- Improve: Rename E2E methods ([#12175](https://github.com/RocketChat/Rocket.Chat/pull/12175)) + +
+ +### 👩‍💻👨‍💻 Contributors 😍 + +- [@MIKI785](https://github.com/MIKI785) +- [@TobiasKappe](https://github.com/TobiasKappe) +- [@aferreira44](https://github.com/aferreira44) +- [@arch119](https://github.com/arch119) +- [@c0dzilla](https://github.com/c0dzilla) +- [@crazy-max](https://github.com/crazy-max) +- [@edzluhan](https://github.com/edzluhan) +- [@flaviogrossi](https://github.com/flaviogrossi) +- [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) +- [@karakayasemi](https://github.com/karakayasemi) +- [@mrinaldhar](https://github.com/mrinaldhar) +- [@ohmonster](https://github.com/ohmonster) +- [@pkgodara](https://github.com/pkgodara) +- [@rndmh3ro](https://github.com/rndmh3ro) +- [@rssilva](https://github.com/rssilva) +- [@thaiphv](https://github.com/thaiphv) +- [@ubarsaiyan](https://github.com/ubarsaiyan) +- [@vynmera](https://github.com/vynmera) +- [@williamriancho](https://github.com/williamriancho) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@Hudell](https://github.com/Hudell) +- [@MarcosSpessatto](https://github.com/MarcosSpessatto) +- [@MartinSchoeler](https://github.com/MartinSchoeler) +- [@cardoso](https://github.com/cardoso) +- [@engelgabriel](https://github.com/engelgabriel) +- [@geekgonecrazy](https://github.com/geekgonecrazy) +- [@ggazzo](https://github.com/ggazzo) +- [@graywolf336](https://github.com/graywolf336) +- [@mrsimpson](https://github.com/mrsimpson) +- [@renatobecker](https://github.com/renatobecker) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@tassoevan](https://github.com/tassoevan) +- [@timkinnane](https://github.com/timkinnane) + +# 0.69.2 +`2018-09-11 · 1 🎉 · 4 🐛 · 6 👩‍💻👨‍💻` + +### Engine versions +- Node: `8.11.3` +- NPM: `5.6.0` + +### 🎉 New features + +- Include room name in stream for bots ([#11812](https://github.com/RocketChat/Rocket.Chat/pull/11812)) + +### 🐛 Bug fixes + +- Reset password link error if already logged in ([#12022](https://github.com/RocketChat/Rocket.Chat/pull/12022)) +- Apps: setting with 'code' type only saving last line ([#11992](https://github.com/RocketChat/Rocket.Chat/pull/11992)) +- Update user information not possible by admin if disabled to users ([#11955](https://github.com/RocketChat/Rocket.Chat/pull/11955) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) +- Hidden admin sidenav on embedded layout ([#12025](https://github.com/RocketChat/Rocket.Chat/pull/12025)) + +### 👩‍💻👨‍💻 Contributors 😍 + +- [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) + +### 👩‍💻👨‍💻 Core Team 🤓 + +- [@cardoso](https://github.com/cardoso) +- [@ggazzo](https://github.com/ggazzo) +- [@rodrigok](https://github.com/rodrigok) +- [@sampaiodiego](https://github.com/sampaiodiego) +- [@timkinnane](https://github.com/timkinnane) + # 0.69.1 `2018-08-31 · 4 🐛 · 2 👩‍💻👨‍💻` @@ -39,7 +2222,7 @@ - Setting to block unauthenticated access to avatars ([#9749](https://github.com/RocketChat/Rocket.Chat/pull/9749)) - Setting to set a JS/CSS CDN ([#11779](https://github.com/RocketChat/Rocket.Chat/pull/11779)) - Make font of unread items bolder for better contrast ([#8602](https://github.com/RocketChat/Rocket.Chat/pull/8602) by [@ausminternet](https://github.com/ausminternet)) -- Internal marketplace for apps ([#11864](https://github.com/RocketChat/Rocket.Chat/pull/11864) by [@gdelavald](https://github.com/gdelavald)) +- Internal marketplace for apps ([#11864](https://github.com/RocketChat/Rocket.Chat/pull/11864) by [@gdelavald](https://github.com/gdelavald) & [@rssilva](https://github.com/rssilva)) ### 🚀 Improvements @@ -61,7 +2244,7 @@ - REST `im.members` endpoint not working without sort parameter ([#11821](https://github.com/RocketChat/Rocket.Chat/pull/11821)) - Livechat rooms starting with two unread message counter ([#11834](https://github.com/RocketChat/Rocket.Chat/pull/11834)) - Results pagination on /directory REST endpoint ([#11551](https://github.com/RocketChat/Rocket.Chat/pull/11551)) -- re-adding margin to menu icon on header ([#11778](https://github.com/RocketChat/Rocket.Chat/pull/11778)) +- re-adding margin to menu icon on header ([#11778](https://github.com/RocketChat/Rocket.Chat/pull/11778) by [@rssilva](https://github.com/rssilva)) - minor fixes in hungarian i18n ([#11797](https://github.com/RocketChat/Rocket.Chat/pull/11797) by [@Atisom](https://github.com/Atisom)) - permissions name no break ([#11836](https://github.com/RocketChat/Rocket.Chat/pull/11836)) - Searching by `undefined` via REST when using `query` param ([#11657](https://github.com/RocketChat/Rocket.Chat/pull/11657)) @@ -82,7 +2265,7 @@ - minor fixes in i18n ([#11761](https://github.com/RocketChat/Rocket.Chat/pull/11761) by [@Atisom](https://github.com/Atisom)) - Code tag duplicating characters ([#11467](https://github.com/RocketChat/Rocket.Chat/pull/11467) by [@vynmera](https://github.com/vynmera)) - Custom sound uploader not working in Firefox and IE ([#11139](https://github.com/RocketChat/Rocket.Chat/pull/11139) by [@vynmera](https://github.com/vynmera)) -- Fixing timeAgo function on directory ([#11728](https://github.com/RocketChat/Rocket.Chat/pull/11728)) +- Fixing timeAgo function on directory ([#11728](https://github.com/RocketChat/Rocket.Chat/pull/11728) by [@rssilva](https://github.com/rssilva)) - Render Attachment Pretext When Markdown Specified ([#11578](https://github.com/RocketChat/Rocket.Chat/pull/11578) by [@glstewart17](https://github.com/glstewart17)) - Message attachments was not respecting sort and lost spacing ([#11740](https://github.com/RocketChat/Rocket.Chat/pull/11740)) - Closed connections being storing on db ([#11709](https://github.com/RocketChat/Rocket.Chat/pull/11709)) @@ -120,6 +2303,7 @@ - [@gsperezb](https://github.com/gsperezb) - [@jukper](https://github.com/jukper) - [@kable-wilmoth](https://github.com/kable-wilmoth) +- [@rssilva](https://github.com/rssilva) - [@ubarsaiyan](https://github.com/ubarsaiyan) - [@ura14h](https://github.com/ura14h) - [@vynmera](https://github.com/vynmera) @@ -135,7 +2319,6 @@ - [@graywolf336](https://github.com/graywolf336) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) -- [@rssilva](https://github.com/rssilva) - [@sampaiodiego](https://github.com/sampaiodiego) - [@tassoevan](https://github.com/tassoevan) - [@timkinnane](https://github.com/timkinnane) @@ -318,7 +2501,7 @@ - Only escape HTML from details in toast error messages ([#11459](https://github.com/RocketChat/Rocket.Chat/pull/11459)) - broadcast channel reply ([#11462](https://github.com/RocketChat/Rocket.Chat/pull/11462)) - Fixed svg for older chrome browsers bug #11414 ([#11416](https://github.com/RocketChat/Rocket.Chat/pull/11416) by [@tpDBL](https://github.com/tpDBL)) -- Wrap custom fields in user profile to new line ([#10119](https://github.com/RocketChat/Rocket.Chat/pull/10119) by [@PhpXp](https://github.com/PhpXp)) +- Wrap custom fields in user profile to new line ([#10119](https://github.com/RocketChat/Rocket.Chat/pull/10119) by [@PhpXp](https://github.com/PhpXp) & [@karlprieb](https://github.com/karlprieb)) - Record popup ([#11349](https://github.com/RocketChat/Rocket.Chat/pull/11349))
@@ -344,6 +2527,7 @@ - [@PhpXp](https://github.com/PhpXp) - [@arminfelder](https://github.com/arminfelder) - [@arungalva](https://github.com/arungalva) +- [@karlprieb](https://github.com/karlprieb) - [@soundstorm](https://github.com/soundstorm) - [@tpDBL](https://github.com/tpDBL) - [@vynmera](https://github.com/vynmera) @@ -358,7 +2542,6 @@ - [@engelgabriel](https://github.com/engelgabriel) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) -- [@karlprieb](https://github.com/karlprieb) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -518,7 +2701,7 @@ - Button to remove closed LiveChat rooms ([#10301](https://github.com/RocketChat/Rocket.Chat/pull/10301)) - Update katex to v0.9.0 ([#8402](https://github.com/RocketChat/Rocket.Chat/pull/8402) by [@pitamar](https://github.com/pitamar)) - WebDAV(Nextcloud/ownCloud) Storage Server Option ([#11027](https://github.com/RocketChat/Rocket.Chat/pull/11027) by [@karakayasemi](https://github.com/karakayasemi)) -- Don't ask me again checkbox on hide room modal ([#10973](https://github.com/RocketChat/Rocket.Chat/pull/10973)) +- Don't ask me again checkbox on hide room modal ([#10973](https://github.com/RocketChat/Rocket.Chat/pull/10973) by [@karlprieb](https://github.com/karlprieb)) - Add input to set time for avatar cache control ([#10958](https://github.com/RocketChat/Rocket.Chat/pull/10958)) - Command /hide to hide channels ([#10727](https://github.com/RocketChat/Rocket.Chat/pull/10727) by [@mikaelmello](https://github.com/mikaelmello)) - Do not wait method calls response on websocket before next method call ([#11087](https://github.com/RocketChat/Rocket.Chat/pull/11087)) @@ -534,8 +2717,8 @@ ### 🚀 Improvements -- Listing of apps in the admin page ([#11166](https://github.com/RocketChat/Rocket.Chat/pull/11166) by [@gdelavald](https://github.com/gdelavald)) -- UI design for Tables and tabs component on Directory ([#11026](https://github.com/RocketChat/Rocket.Chat/pull/11026)) +- Listing of apps in the admin page ([#11166](https://github.com/RocketChat/Rocket.Chat/pull/11166) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) +- UI design for Tables and tabs component on Directory ([#11026](https://github.com/RocketChat/Rocket.Chat/pull/11026) by [@karlprieb](https://github.com/karlprieb)) - User mentions ([#11001](https://github.com/RocketChat/Rocket.Chat/pull/11001) by [@vynmera](https://github.com/vynmera)) ### 🐛 Bug fixes @@ -570,7 +2753,7 @@ - Message_AllowedMaxSize fails for emoji sequences ([#10431](https://github.com/RocketChat/Rocket.Chat/pull/10431) by [@c0dzilla](https://github.com/c0dzilla)) - Can't access the `/account/profile` ([#11089](https://github.com/RocketChat/Rocket.Chat/pull/11089)) - Idle time limit wasn’t working as expected ([#11084](https://github.com/RocketChat/Rocket.Chat/pull/11084)) -- Rooms list sorting by activity multiple re-renders and case sensitive sorting alphabetically ([#9959](https://github.com/RocketChat/Rocket.Chat/pull/9959) by [@JoseRenan](https://github.com/JoseRenan)) +- Rooms list sorting by activity multiple re-renders and case sensitive sorting alphabetically ([#9959](https://github.com/RocketChat/Rocket.Chat/pull/9959) by [@JoseRenan](https://github.com/JoseRenan) & [@karlprieb](https://github.com/karlprieb)) - Notification not working for group mentions and not respecting ignored users ([#11024](https://github.com/RocketChat/Rocket.Chat/pull/11024)) - Overlapping of search text and cancel search icon (X) ([#10294](https://github.com/RocketChat/Rocket.Chat/pull/10294) by [@taeven](https://github.com/taeven)) - Link previews not being removed from messages after removed on editing ([#11063](https://github.com/RocketChat/Rocket.Chat/pull/11063)) @@ -600,11 +2783,11 @@ 🔍 Minor changes - Merge master into develop & Set version to 0.66.0-develop ([#11277](https://github.com/RocketChat/Rocket.Chat/pull/11277) by [@brylie](https://github.com/brylie) & [@stuartpb](https://github.com/stuartpb)) -- Regression: Directory css ([#11206](https://github.com/RocketChat/Rocket.Chat/pull/11206)) +- Regression: Directory css ([#11206](https://github.com/RocketChat/Rocket.Chat/pull/11206) by [@karlprieb](https://github.com/karlprieb)) - LingoHub based on develop ([#11208](https://github.com/RocketChat/Rocket.Chat/pull/11208)) - IRC Federation: RFC2813 implementation (ngIRCd) ([#10113](https://github.com/RocketChat/Rocket.Chat/pull/10113) by [@cpitman](https://github.com/cpitman) & [@lindoelio](https://github.com/lindoelio)) - Add verification to make sure the user exists in REST insert object helper ([#11008](https://github.com/RocketChat/Rocket.Chat/pull/11008)) -- Regression: Directory user table infinite scroll doesn't working ([#11200](https://github.com/RocketChat/Rocket.Chat/pull/11200)) +- Regression: Directory user table infinite scroll doesn't working ([#11200](https://github.com/RocketChat/Rocket.Chat/pull/11200) by [@karlprieb](https://github.com/karlprieb)) - [FIX Readme] Nodejs + Python version spicifications ([#11181](https://github.com/RocketChat/Rocket.Chat/pull/11181) by [@mahdiyari](https://github.com/mahdiyari)) - Regression: sorting direct message by asc on favorites group ([#11090](https://github.com/RocketChat/Rocket.Chat/pull/11090)) - Fix PR Docker image creation by splitting in two build jobs ([#11107](https://github.com/RocketChat/Rocket.Chat/pull/11107)) @@ -628,12 +2811,12 @@ - NPM Dependencies Update ([#10913](https://github.com/RocketChat/Rocket.Chat/pull/10913)) - update meteor to 1.6.1 for sandstorm build ([#10131](https://github.com/RocketChat/Rocket.Chat/pull/10131) by [@peterlee0127](https://github.com/peterlee0127)) - Renaming username.username to username.value for clarity ([#10986](https://github.com/RocketChat/Rocket.Chat/pull/10986)) -- Fix readme typo ([#5](https://github.com/RocketChat/Rocket.Chat/pull/5)) +- Fix readme typo ([#5](https://github.com/RocketChat/Rocket.Chat/pull/5) by [@filipealva](https://github.com/filipealva)) - Remove wrong and not needed time unit ([#10807](https://github.com/RocketChat/Rocket.Chat/pull/10807) by [@cliffparnitzky](https://github.com/cliffparnitzky)) -- Develop sync commits ([#10909](https://github.com/RocketChat/Rocket.Chat/pull/10909) by [@nsuchy](https://github.com/nsuchy)) -- Develop sync2 ([#10908](https://github.com/RocketChat/Rocket.Chat/pull/10908) by [@nsuchy](https://github.com/nsuchy)) -- Merge master into develop & Set version to 0.66.0-develop ([#10903](https://github.com/RocketChat/Rocket.Chat/pull/10903) by [@nsuchy](https://github.com/nsuchy)) -- Regression: Fix directory table loading ([#11223](https://github.com/RocketChat/Rocket.Chat/pull/11223)) +- Develop sync commits ([#10909](https://github.com/RocketChat/Rocket.Chat/pull/10909) by [@nsuchy](https://github.com/nsuchy) & [@rafaelks](https://github.com/rafaelks)) +- Develop sync2 ([#10908](https://github.com/RocketChat/Rocket.Chat/pull/10908) by [@nsuchy](https://github.com/nsuchy) & [@rafaelks](https://github.com/rafaelks)) +- Merge master into develop & Set version to 0.66.0-develop ([#10903](https://github.com/RocketChat/Rocket.Chat/pull/10903) by [@nsuchy](https://github.com/nsuchy) & [@rafaelks](https://github.com/rafaelks)) +- Regression: Fix directory table loading ([#11223](https://github.com/RocketChat/Rocket.Chat/pull/11223) by [@karlprieb](https://github.com/karlprieb)) - Regression: Fix latest and release-candidate docker images building ([#11215](https://github.com/RocketChat/Rocket.Chat/pull/11215)) - Regression: check username or usersCount on browseChannels ([#11216](https://github.com/RocketChat/Rocket.Chat/pull/11216)) - Regression: Sending message with a mention is not showing to sender ([#11211](https://github.com/RocketChat/Rocket.Chat/pull/11211)) @@ -656,11 +2839,13 @@ - [@c0dzilla](https://github.com/c0dzilla) - [@cliffparnitzky](https://github.com/cliffparnitzky) - [@cpitman](https://github.com/cpitman) +- [@filipealva](https://github.com/filipealva) - [@gdelavald](https://github.com/gdelavald) - [@haffla](https://github.com/haffla) - [@jonnilundy](https://github.com/jonnilundy) - [@kable-wilmoth](https://github.com/kable-wilmoth) - [@karakayasemi](https://github.com/karakayasemi) +- [@karlprieb](https://github.com/karlprieb) - [@kb0304](https://github.com/kb0304) - [@kumarnitj](https://github.com/kumarnitj) - [@lindoelio](https://github.com/lindoelio) @@ -673,6 +2858,7 @@ - [@peterlee0127](https://github.com/peterlee0127) - [@pitamar](https://github.com/pitamar) - [@pkgodara](https://github.com/pkgodara) +- [@rafaelks](https://github.com/rafaelks) - [@rakhi2104](https://github.com/rakhi2104) - [@rw4lll](https://github.com/rw4lll) - [@saplla](https://github.com/saplla) @@ -688,12 +2874,9 @@ - [@alansikora](https://github.com/alansikora) - [@cardoso](https://github.com/cardoso) - [@engelgabriel](https://github.com/engelgabriel) -- [@filipealva](https://github.com/filipealva) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) -- [@rafaelks](https://github.com/rafaelks) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -761,8 +2944,8 @@ - Implement a local password policy ([#9857](https://github.com/RocketChat/Rocket.Chat/pull/9857)) - Options to enable/disable each Livechat registration form field ([#10584](https://github.com/RocketChat/Rocket.Chat/pull/10584)) - Return the result of the `/me` endpoint within the result of the `/login` endpoint ([#10677](https://github.com/RocketChat/Rocket.Chat/pull/10677)) -- Lazy load image attachments ([#10608](https://github.com/RocketChat/Rocket.Chat/pull/10608)) -- View pinned message's attachment ([#10214](https://github.com/RocketChat/Rocket.Chat/pull/10214) by [@c0dzilla](https://github.com/c0dzilla)) +- Lazy load image attachments ([#10608](https://github.com/RocketChat/Rocket.Chat/pull/10608) by [@karlprieb](https://github.com/karlprieb)) +- View pinned message's attachment ([#10214](https://github.com/RocketChat/Rocket.Chat/pull/10214) by [@c0dzilla](https://github.com/c0dzilla) & [@karlprieb](https://github.com/karlprieb)) - Add REST API endpoint `users.getUsernameSuggestion` to get username suggestion ([#10702](https://github.com/RocketChat/Rocket.Chat/pull/10702)) - REST API endpoint `settings` now allow set colors and trigger actions ([#10488](https://github.com/RocketChat/Rocket.Chat/pull/10488) by [@ThomasRoehl](https://github.com/ThomasRoehl)) - Add REST endpoint `subscriptions.unread` to mark messages as unread ([#10778](https://github.com/RocketChat/Rocket.Chat/pull/10778)) @@ -784,7 +2967,7 @@ - Internal Error when requesting user data download ([#10837](https://github.com/RocketChat/Rocket.Chat/pull/10837)) - Broadcast channels were showing reply button for deleted messages and generating wrong reply links some times ([#10835](https://github.com/RocketChat/Rocket.Chat/pull/10835)) - User's preference `Unread on Top` wasn't working for LiveChat rooms ([#10734](https://github.com/RocketChat/Rocket.Chat/pull/10734)) -- Cancel button wasn't working while uploading file ([#10715](https://github.com/RocketChat/Rocket.Chat/pull/10715) by [@Mr-Gryphon](https://github.com/Mr-Gryphon)) +- Cancel button wasn't working while uploading file ([#10715](https://github.com/RocketChat/Rocket.Chat/pull/10715) by [@Mr-Gryphon](https://github.com/Mr-Gryphon) & [@karlprieb](https://github.com/karlprieb)) - Missing pagination fields in the response of REST /directory endpoint ([#10840](https://github.com/RocketChat/Rocket.Chat/pull/10840)) - Layout badge cutting on unread messages for long names ([#10846](https://github.com/RocketChat/Rocket.Chat/pull/10846) by [@kos4live](https://github.com/kos4live)) - Slack-Bridge bug when migrating to 0.64.1 ([#10875](https://github.com/RocketChat/Rocket.Chat/pull/10875)) @@ -794,9 +2977,9 @@
🔍 Minor changes -- Release 0.65.0 ([#10893](https://github.com/RocketChat/Rocket.Chat/pull/10893) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan)) +- Release 0.65.0 ([#10893](https://github.com/RocketChat/Rocket.Chat/pull/10893) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan)) - Apps: Command Previews, Message and Room Removal Events ([#10822](https://github.com/RocketChat/Rocket.Chat/pull/10822)) -- Develop sync ([#10815](https://github.com/RocketChat/Rocket.Chat/pull/10815) by [@nsuchy](https://github.com/nsuchy)) +- Develop sync ([#10815](https://github.com/RocketChat/Rocket.Chat/pull/10815) by [@nsuchy](https://github.com/nsuchy) & [@rafaelks](https://github.com/rafaelks)) - Major dependencies update ([#10661](https://github.com/RocketChat/Rocket.Chat/pull/10661)) - Prevent setup wizard redirects ([#10811](https://github.com/RocketChat/Rocket.Chat/pull/10811)) - Fix: Regression in REST API endpoint `/me` ([#10833](https://github.com/RocketChat/Rocket.Chat/pull/10833)) @@ -808,7 +2991,7 @@ - Fix: Manage apps layout was a bit confuse ([#10882](https://github.com/RocketChat/Rocket.Chat/pull/10882) by [@gdelavald](https://github.com/gdelavald)) - LingoHub based on develop ([#10886](https://github.com/RocketChat/Rocket.Chat/pull/10886)) - Fix: Regression Lazyload fix shuffle avatars ([#10887](https://github.com/RocketChat/Rocket.Chat/pull/10887)) -- Fix: typo on error message for push token API ([#10857](https://github.com/RocketChat/Rocket.Chat/pull/10857)) +- Fix: typo on error message for push token API ([#10857](https://github.com/RocketChat/Rocket.Chat/pull/10857) by [@rafaelks](https://github.com/rafaelks))
@@ -822,9 +3005,11 @@ - [@chuckAtCataworx](https://github.com/chuckAtCataworx) - [@erhan-](https://github.com/erhan-) - [@gdelavald](https://github.com/gdelavald) +- [@karlprieb](https://github.com/karlprieb) - [@kos4live](https://github.com/kos4live) - [@nsuchy](https://github.com/nsuchy) - [@peccu](https://github.com/peccu) +- [@rafaelks](https://github.com/rafaelks) - [@winterstefan](https://github.com/winterstefan) - [@xbolshe](https://github.com/xbolshe) @@ -837,8 +3022,6 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) -- [@rafaelks](https://github.com/rafaelks) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -852,13 +3035,13 @@ ### 🎉 New features -- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607)) +- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607) by [@rafaelks](https://github.com/rafaelks)) - Add more options for Wordpress OAuth configuration ([#10724](https://github.com/RocketChat/Rocket.Chat/pull/10724)) -- Setup Wizard ([#10523](https://github.com/RocketChat/Rocket.Chat/pull/10523)) +- Setup Wizard ([#10523](https://github.com/RocketChat/Rocket.Chat/pull/10523) by [@karlprieb](https://github.com/karlprieb)) - Improvements to notifications logic ([#10686](https://github.com/RocketChat/Rocket.Chat/pull/10686)) -- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607)) +- Add REST endpoints `channels.roles` & `groups.roles` ([#10607](https://github.com/RocketChat/Rocket.Chat/pull/10607) by [@rafaelks](https://github.com/rafaelks)) - Add more options for Wordpress OAuth configuration ([#10724](https://github.com/RocketChat/Rocket.Chat/pull/10724)) -- Setup Wizard ([#10523](https://github.com/RocketChat/Rocket.Chat/pull/10523)) +- Setup Wizard ([#10523](https://github.com/RocketChat/Rocket.Chat/pull/10523) by [@karlprieb](https://github.com/karlprieb)) - Improvements to notifications logic ([#10686](https://github.com/RocketChat/Rocket.Chat/pull/10686)) ### 🐛 Bug fixes @@ -883,7 +3066,7 @@
🔍 Minor changes -- Release 0.64.2 ([#10812](https://github.com/RocketChat/Rocket.Chat/pull/10812) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan)) +- Release 0.64.2 ([#10812](https://github.com/RocketChat/Rocket.Chat/pull/10812) by [@Sameesunkaria](https://github.com/Sameesunkaria) & [@erhan-](https://github.com/erhan-) & [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb) & [@peccu](https://github.com/peccu) & [@winterstefan](https://github.com/winterstefan)) - Prometheus: Add metric to track hooks time ([#10798](https://github.com/RocketChat/Rocket.Chat/pull/10798)) - Regression: Autorun of wizard was not destroyed after completion ([#10802](https://github.com/RocketChat/Rocket.Chat/pull/10802)) - Prometheus: Fix notification metric ([#10803](https://github.com/RocketChat/Rocket.Chat/pull/10803)) @@ -922,7 +3105,9 @@ - [@Sameesunkaria](https://github.com/Sameesunkaria) - [@erhan-](https://github.com/erhan-) - [@gdelavald](https://github.com/gdelavald) +- [@karlprieb](https://github.com/karlprieb) - [@peccu](https://github.com/peccu) +- [@rafaelks](https://github.com/rafaelks) - [@winterstefan](https://github.com/winterstefan) ### 👩‍💻👨‍💻 Core Team 🤓 @@ -931,8 +3116,6 @@ - [@MarcosSpessatto](https://github.com/MarcosSpessatto) - [@cardoso](https://github.com/cardoso) - [@engelgabriel](https://github.com/engelgabriel) -- [@karlprieb](https://github.com/karlprieb) -- [@rafaelks](https://github.com/rafaelks) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -990,7 +3173,7 @@ - Option to mute group mentions (@all and @here) ([#10502](https://github.com/RocketChat/Rocket.Chat/pull/10502)) - GDPR - Right to access and Data Portability ([#9906](https://github.com/RocketChat/Rocket.Chat/pull/9906)) - Broadcast Channels ([#9950](https://github.com/RocketChat/Rocket.Chat/pull/9950)) -- Option to ignore users on channels ([#10517](https://github.com/RocketChat/Rocket.Chat/pull/10517) by [@gdelavald](https://github.com/gdelavald)) +- Option to ignore users on channels ([#10517](https://github.com/RocketChat/Rocket.Chat/pull/10517) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) - Search Provider Framework ([#10110](https://github.com/RocketChat/Rocket.Chat/pull/10110) by [@tkurz](https://github.com/tkurz)) - REST API endpoint `/directory` ([#10442](https://github.com/RocketChat/Rocket.Chat/pull/10442)) - Body of the payload on an incoming webhook is included on the request object ([#10259](https://github.com/RocketChat/Rocket.Chat/pull/10259)) @@ -1016,36 +3199,36 @@ - Rename method to clean history of messages ([#10498](https://github.com/RocketChat/Rocket.Chat/pull/10498)) - REST spotlight API wasn't allowing searches with # and @ ([#10410](https://github.com/RocketChat/Rocket.Chat/pull/10410)) - Dropdown elements were using old styles ([#10482](https://github.com/RocketChat/Rocket.Chat/pull/10482) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) -- Directory sort and column sizes were wrong ([#10403](https://github.com/RocketChat/Rocket.Chat/pull/10403)) +- Directory sort and column sizes were wrong ([#10403](https://github.com/RocketChat/Rocket.Chat/pull/10403) by [@karlprieb](https://github.com/karlprieb)) - REST API OAuth services endpoint were missing fields and flag to indicate custom services ([#10299](https://github.com/RocketChat/Rocket.Chat/pull/10299)) -- Error messages weren't been displayed when email verification fails ([#10446](https://github.com/RocketChat/Rocket.Chat/pull/10446)) -- Wrong column positions in the directory search for users ([#10454](https://github.com/RocketChat/Rocket.Chat/pull/10454) by [@lunaticmonk](https://github.com/lunaticmonk)) +- Error messages weren't been displayed when email verification fails ([#10446](https://github.com/RocketChat/Rocket.Chat/pull/10446) by [@karlprieb](https://github.com/karlprieb)) +- Wrong column positions in the directory search for users ([#10454](https://github.com/RocketChat/Rocket.Chat/pull/10454) by [@karlprieb](https://github.com/karlprieb) & [@lunaticmonk](https://github.com/lunaticmonk)) - Custom fields was misaligned in registration form ([#10463](https://github.com/RocketChat/Rocket.Chat/pull/10463) by [@dschuan](https://github.com/dschuan)) - Unique identifier file not really being unique ([#10341](https://github.com/RocketChat/Rocket.Chat/pull/10341) by [@abernix](https://github.com/abernix)) - Empty panel after changing a user's username ([#10404](https://github.com/RocketChat/Rocket.Chat/pull/10404)) - Russian translation of "False" ([#10418](https://github.com/RocketChat/Rocket.Chat/pull/10418) by [@strangerintheq](https://github.com/strangerintheq)) - Links being embedded inside of blockquotes ([#10496](https://github.com/RocketChat/Rocket.Chat/pull/10496) by [@gdelavald](https://github.com/gdelavald)) -- The 'channel.messages' REST API Endpoint error ([#10485](https://github.com/RocketChat/Rocket.Chat/pull/10485)) -- Button on user info contextual bar scrolling with the content ([#10358](https://github.com/RocketChat/Rocket.Chat/pull/10358) by [@okaybroda](https://github.com/okaybroda)) +- The 'channel.messages' REST API Endpoint error ([#10485](https://github.com/RocketChat/Rocket.Chat/pull/10485) by [@rafaelks](https://github.com/rafaelks)) +- Button on user info contextual bar scrolling with the content ([#10358](https://github.com/RocketChat/Rocket.Chat/pull/10358) by [@karlprieb](https://github.com/karlprieb) & [@okaybroda](https://github.com/okaybroda)) - "Idle Time Limit" using milliseconds instead of seconds ([#9824](https://github.com/RocketChat/Rocket.Chat/pull/9824) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) - Missing i18n translation key for "Unread" ([#10387](https://github.com/RocketChat/Rocket.Chat/pull/10387)) - Owner unable to delete channel or group from APIs ([#9729](https://github.com/RocketChat/Rocket.Chat/pull/9729) by [@c0dzilla](https://github.com/c0dzilla)) - Livechat translation files being ignored ([#10369](https://github.com/RocketChat/Rocket.Chat/pull/10369)) -- Missing page "not found" ([#6673](https://github.com/RocketChat/Rocket.Chat/pull/6673) by [@Prakharsvnit](https://github.com/Prakharsvnit)) +- Missing page "not found" ([#6673](https://github.com/RocketChat/Rocket.Chat/pull/6673) by [@Prakharsvnit](https://github.com/Prakharsvnit) & [@karlprieb](https://github.com/karlprieb)) - "Highlight Words" wasn't working with more than one word ([#10083](https://github.com/RocketChat/Rocket.Chat/pull/10083) by [@gdelavald](https://github.com/gdelavald) & [@nemaniarjun](https://github.com/nemaniarjun)) -- Missing "Administration" menu for user with manage-emoji permission ([#10171](https://github.com/RocketChat/Rocket.Chat/pull/10171) by [@c0dzilla](https://github.com/c0dzilla)) -- Message view mode setting was missing at user's preferences ([#10395](https://github.com/RocketChat/Rocket.Chat/pull/10395) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) -- Profile image was not being shown in user's directory search ([#10399](https://github.com/RocketChat/Rocket.Chat/pull/10399) by [@lunaticmonk](https://github.com/lunaticmonk)) -- Wrong positioning of popover when using RTL languages ([#10428](https://github.com/RocketChat/Rocket.Chat/pull/10428)) -- Messages was grouping wrong some times when server is slow ([#10472](https://github.com/RocketChat/Rocket.Chat/pull/10472) by [@gdelavald](https://github.com/gdelavald)) -- GitLab authentication scope was too open, reduced to read only access ([#10225](https://github.com/RocketChat/Rocket.Chat/pull/10225)) +- Missing "Administration" menu for user with manage-emoji permission ([#10171](https://github.com/RocketChat/Rocket.Chat/pull/10171) by [@c0dzilla](https://github.com/c0dzilla) & [@karlprieb](https://github.com/karlprieb)) +- Message view mode setting was missing at user's preferences ([#10395](https://github.com/RocketChat/Rocket.Chat/pull/10395) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) & [@karlprieb](https://github.com/karlprieb)) +- Profile image was not being shown in user's directory search ([#10399](https://github.com/RocketChat/Rocket.Chat/pull/10399) by [@karlprieb](https://github.com/karlprieb) & [@lunaticmonk](https://github.com/lunaticmonk)) +- Wrong positioning of popover when using RTL languages ([#10428](https://github.com/RocketChat/Rocket.Chat/pull/10428) by [@karlprieb](https://github.com/karlprieb)) +- Messages was grouping wrong some times when server is slow ([#10472](https://github.com/RocketChat/Rocket.Chat/pull/10472) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) +- GitLab authentication scope was too open, reduced to read only access ([#10225](https://github.com/RocketChat/Rocket.Chat/pull/10225) by [@rafaelks](https://github.com/rafaelks)) - Renaming agent's username within Livechat's department ([#10344](https://github.com/RocketChat/Rocket.Chat/pull/10344)) -- Missing RocketApps input types ([#10394](https://github.com/RocketChat/Rocket.Chat/pull/10394)) +- Missing RocketApps input types ([#10394](https://github.com/RocketChat/Rocket.Chat/pull/10394) by [@karlprieb](https://github.com/karlprieb)) - Livechat desktop notifications not being displayed ([#10221](https://github.com/RocketChat/Rocket.Chat/pull/10221)) -- Autocomplete list when inviting a user was partial hidden ([#10409](https://github.com/RocketChat/Rocket.Chat/pull/10409)) -- Remove a user from the user's list when creating a new channel removes the wrong user ([#10423](https://github.com/RocketChat/Rocket.Chat/pull/10423) by [@gdelavald](https://github.com/gdelavald)) +- Autocomplete list when inviting a user was partial hidden ([#10409](https://github.com/RocketChat/Rocket.Chat/pull/10409) by [@karlprieb](https://github.com/karlprieb)) +- Remove a user from the user's list when creating a new channel removes the wrong user ([#10423](https://github.com/RocketChat/Rocket.Chat/pull/10423) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) - Room's name was cutting instead of having ellipses on sidebar ([#10430](https://github.com/RocketChat/Rocket.Chat/pull/10430)) -- Button to delete rooms by the owners wasn't appearing ([#10438](https://github.com/RocketChat/Rocket.Chat/pull/10438)) +- Button to delete rooms by the owners wasn't appearing ([#10438](https://github.com/RocketChat/Rocket.Chat/pull/10438) by [@karlprieb](https://github.com/karlprieb)) - Updated OpenShift Template to take an Image as a Param ([#9946](https://github.com/RocketChat/Rocket.Chat/pull/9946) by [@christianh814](https://github.com/christianh814)) - Incoming integrations being able to trigger an empty message with a GET ([#9576](https://github.com/RocketChat/Rocket.Chat/pull/9576)) - Snaps installations are breaking on avatar requests ([#10390](https://github.com/RocketChat/Rocket.Chat/pull/10390)) @@ -1062,7 +3245,7 @@ - Regression: Apps and Livechats not getting along well with each other ([#10598](https://github.com/RocketChat/Rocket.Chat/pull/10598)) - Development: Add Visual Studio Code debugging configuration ([#10586](https://github.com/RocketChat/Rocket.Chat/pull/10586)) - Included missing lib for migrations ([#10532](https://github.com/RocketChat/Rocket.Chat/pull/10532)) -- Develop sync ([#10505](https://github.com/RocketChat/Rocket.Chat/pull/10505) by [@nsuchy](https://github.com/nsuchy)) +- Develop sync ([#10505](https://github.com/RocketChat/Rocket.Chat/pull/10505) by [@nsuchy](https://github.com/nsuchy) & [@rafaelks](https://github.com/rafaelks)) - Fix: Remove "secret" from REST endpoint /settings.oauth response ([#10513](https://github.com/RocketChat/Rocket.Chat/pull/10513)) - [OTHER] More Listeners for Apps & Utilize Promises inside Apps ([#10335](https://github.com/RocketChat/Rocket.Chat/pull/10335)) - [OTHER] Develop sync ([#10487](https://github.com/RocketChat/Rocket.Chat/pull/10487)) @@ -1101,10 +3284,12 @@ - [@dschuan](https://github.com/dschuan) - [@gdelavald](https://github.com/gdelavald) - [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) +- [@karlprieb](https://github.com/karlprieb) - [@lunaticmonk](https://github.com/lunaticmonk) - [@nemaniarjun](https://github.com/nemaniarjun) - [@nsuchy](https://github.com/nsuchy) - [@okaybroda](https://github.com/okaybroda) +- [@rafaelks](https://github.com/rafaelks) - [@strangerintheq](https://github.com/strangerintheq) - [@t3hchipmunk](https://github.com/t3hchipmunk) - [@tkurz](https://github.com/tkurz) @@ -1120,8 +3305,6 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) -- [@rafaelks](https://github.com/rafaelks) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -1136,14 +3319,17 @@
🔍 Minor changes -- Release 0.63.3 ([#10504](https://github.com/RocketChat/Rocket.Chat/pull/10504)) +- Release 0.63.3 ([#10504](https://github.com/RocketChat/Rocket.Chat/pull/10504) by [@rafaelks](https://github.com/rafaelks))
+### 👩‍💻👨‍💻 Contributors 😍 + +- [@rafaelks](https://github.com/rafaelks) + ### 👩‍💻👨‍💻 Core Team 🤓 - [@graywolf336](https://github.com/graywolf336) -- [@rafaelks](https://github.com/rafaelks) # 0.63.2 `2018-04-17 · 2 🔍 · 2 👩‍💻👨‍💻` @@ -1222,16 +3408,16 @@ - Audio recording as mp3 and better ui for recording ([#9726](https://github.com/RocketChat/Rocket.Chat/pull/9726) by [@kb0304](https://github.com/kb0304)) - Setting to configure max delta for 2fa ([#9732](https://github.com/RocketChat/Rocket.Chat/pull/9732)) - Livechat webhook request on message ([#9870](https://github.com/RocketChat/Rocket.Chat/pull/9870) by [@hmagarotto](https://github.com/hmagarotto)) -- Announcement bar color wasn't using color from theming variables ([#9367](https://github.com/RocketChat/Rocket.Chat/pull/9367) by [@cyclops24](https://github.com/cyclops24)) +- Announcement bar color wasn't using color from theming variables ([#9367](https://github.com/RocketChat/Rocket.Chat/pull/9367) by [@cyclops24](https://github.com/cyclops24) & [@karlprieb](https://github.com/karlprieb)) ### 🐛 Bug fixes - Audio Message UI fixes ([#10303](https://github.com/RocketChat/Rocket.Chat/pull/10303) by [@kb0304](https://github.com/kb0304)) -- "View All Members" button inside channel's "User Info" is over sized ([#10012](https://github.com/RocketChat/Rocket.Chat/pull/10012)) +- "View All Members" button inside channel's "User Info" is over sized ([#10012](https://github.com/RocketChat/Rocket.Chat/pull/10012) by [@karlprieb](https://github.com/karlprieb)) - Apostrophe-containing URL misparsed" ([#10242](https://github.com/RocketChat/Rocket.Chat/pull/10242)) - user status on sidenav ([#10222](https://github.com/RocketChat/Rocket.Chat/pull/10222)) -- Dynamic CSS script isn't working on older browsers ([#10152](https://github.com/RocketChat/Rocket.Chat/pull/10152)) -- Extended view mode on sidebar ([#10160](https://github.com/RocketChat/Rocket.Chat/pull/10160)) +- Dynamic CSS script isn't working on older browsers ([#10152](https://github.com/RocketChat/Rocket.Chat/pull/10152) by [@karlprieb](https://github.com/karlprieb)) +- Extended view mode on sidebar ([#10160](https://github.com/RocketChat/Rocket.Chat/pull/10160) by [@karlprieb](https://github.com/karlprieb)) - Cannot answer to a livechat as a manager if agent has not answered yet ([#10082](https://github.com/RocketChat/Rocket.Chat/pull/10082) by [@kb0304](https://github.com/kb0304)) - User status missing on user info ([#9866](https://github.com/RocketChat/Rocket.Chat/pull/9866) by [@lunaticmonk](https://github.com/lunaticmonk)) - Name of files in file upload list cuts down at bottom due to overflow ([#9672](https://github.com/RocketChat/Rocket.Chat/pull/9672) by [@lunaticmonk](https://github.com/lunaticmonk)) @@ -1242,12 +3428,12 @@ - Broken video call accept dialog ([#9872](https://github.com/RocketChat/Rocket.Chat/pull/9872) by [@ramrami](https://github.com/ramrami)) - Wrong switch button border color ([#10081](https://github.com/RocketChat/Rocket.Chat/pull/10081) by [@kb0304](https://github.com/kb0304)) - Nextcloud as custom oauth provider wasn't mapping data correctly ([#10090](https://github.com/RocketChat/Rocket.Chat/pull/10090)) -- Missing sidebar default options on admin ([#10016](https://github.com/RocketChat/Rocket.Chat/pull/10016)) +- Missing sidebar default options on admin ([#10016](https://github.com/RocketChat/Rocket.Chat/pull/10016) by [@karlprieb](https://github.com/karlprieb)) - Able to react with invalid emoji ([#8667](https://github.com/RocketChat/Rocket.Chat/pull/8667) by [@mutdmour](https://github.com/mutdmour)) - User preferences can't be saved when roles are hidden in admin settings ([#10051](https://github.com/RocketChat/Rocket.Chat/pull/10051)) - Browser was auto-filling values when editing another user profile ([#9932](https://github.com/RocketChat/Rocket.Chat/pull/9932) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) -- Avatar input was accepting not supported image types ([#10011](https://github.com/RocketChat/Rocket.Chat/pull/10011)) -- Initial loading feedback was missing ([#10028](https://github.com/RocketChat/Rocket.Chat/pull/10028)) +- Avatar input was accepting not supported image types ([#10011](https://github.com/RocketChat/Rocket.Chat/pull/10011) by [@karlprieb](https://github.com/karlprieb)) +- Initial loading feedback was missing ([#10028](https://github.com/RocketChat/Rocket.Chat/pull/10028) by [@karlprieb](https://github.com/karlprieb)) - File had redirect delay when using external storage services and no option to proxy only avatars ([#10272](https://github.com/RocketChat/Rocket.Chat/pull/10272)) - Missing pt-BR translations ([#10262](https://github.com/RocketChat/Rocket.Chat/pull/10262)) - /me REST endpoint was missing user roles and preferences ([#10240](https://github.com/RocketChat/Rocket.Chat/pull/10240)) @@ -1260,7 +3446,7 @@
🔍 Minor changes -- Release 0.63.0 ([#10324](https://github.com/RocketChat/Rocket.Chat/pull/10324) by [@Joe-mcgee](https://github.com/Joe-mcgee) & [@TopHattedCat](https://github.com/TopHattedCat) & [@hmagarotto](https://github.com/hmagarotto) & [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) & [@kb0304](https://github.com/kb0304) & [@lunaticmonk](https://github.com/lunaticmonk) & [@ramrami](https://github.com/ramrami)) +- Release 0.63.0 ([#10324](https://github.com/RocketChat/Rocket.Chat/pull/10324) by [@Joe-mcgee](https://github.com/Joe-mcgee) & [@TopHattedCat](https://github.com/TopHattedCat) & [@hmagarotto](https://github.com/hmagarotto) & [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) & [@karlprieb](https://github.com/karlprieb) & [@kb0304](https://github.com/kb0304) & [@lunaticmonk](https://github.com/lunaticmonk) & [@ramrami](https://github.com/ramrami)) - Fix: Reaction endpoint/api only working with regular emojis ([#10323](https://github.com/RocketChat/Rocket.Chat/pull/10323)) - Bump snap version to include security fix ([#10313](https://github.com/RocketChat/Rocket.Chat/pull/10313)) - Update Meteor to 1.6.1.1 ([#10314](https://github.com/RocketChat/Rocket.Chat/pull/10314)) @@ -1271,7 +3457,7 @@ - Add forums as a place to suggest, discuss and upvote features ([#10148](https://github.com/RocketChat/Rocket.Chat/pull/10148) by [@SeanPackham](https://github.com/SeanPackham)) - Fix tests breaking randomly ([#10065](https://github.com/RocketChat/Rocket.Chat/pull/10065)) - [OTHER] Reactivate all tests ([#10036](https://github.com/RocketChat/Rocket.Chat/pull/10036)) -- [OTHER] Reactivate API tests ([#9844](https://github.com/RocketChat/Rocket.Chat/pull/9844)) +- [OTHER] Reactivate API tests ([#9844](https://github.com/RocketChat/Rocket.Chat/pull/9844) by [@karlprieb](https://github.com/karlprieb)) - Start 0.63.0-develop / develop sync from master ([#9985](https://github.com/RocketChat/Rocket.Chat/pull/9985)) - Fix: Renaming channels.notifications Get/Post endpoints ([#10257](https://github.com/RocketChat/Rocket.Chat/pull/10257)) - Fix caddy download link to pull from github ([#10260](https://github.com/RocketChat/Rocket.Chat/pull/10260)) @@ -1293,6 +3479,7 @@ - [@cyclops24](https://github.com/cyclops24) - [@hmagarotto](https://github.com/hmagarotto) - [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) +- [@karlprieb](https://github.com/karlprieb) - [@kb0304](https://github.com/kb0304) - [@lunaticmonk](https://github.com/lunaticmonk) - [@mutdmour](https://github.com/mutdmour) @@ -1307,7 +3494,6 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@pierreozoux](https://github.com/pierreozoux) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) @@ -1356,7 +3542,7 @@ ### 🐛 Bug fixes - Delete user without username was removing direct rooms of all users ([#9986](https://github.com/RocketChat/Rocket.Chat/pull/9986)) -- New channel page on medium size screens ([#9988](https://github.com/RocketChat/Rocket.Chat/pull/9988)) +- New channel page on medium size screens ([#9988](https://github.com/RocketChat/Rocket.Chat/pull/9988) by [@karlprieb](https://github.com/karlprieb)) - Empty sidenav when sorting by activity and there is a subscription without room ([#9960](https://github.com/RocketChat/Rocket.Chat/pull/9960)) - Two factor authentication modal was not showing ([#9982](https://github.com/RocketChat/Rocket.Chat/pull/9982)) @@ -1367,10 +3553,13 @@
+### 👩‍💻👨‍💻 Contributors 😍 + +- [@karlprieb](https://github.com/karlprieb) + ### 👩‍💻👨‍💻 Core Team 🤓 - [@ggazzo](https://github.com/ggazzo) -- [@karlprieb](https://github.com/karlprieb) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -1389,9 +3578,9 @@ - Version update check ([#9793](https://github.com/RocketChat/Rocket.Chat/pull/9793)) - General alert banner ([#9778](https://github.com/RocketChat/Rocket.Chat/pull/9778)) -- Browse more channels / Directory ([#9642](https://github.com/RocketChat/Rocket.Chat/pull/9642)) +- Browse more channels / Directory ([#9642](https://github.com/RocketChat/Rocket.Chat/pull/9642) by [@karlprieb](https://github.com/karlprieb)) - Add user settings / preferences API endpoint ([#9457](https://github.com/RocketChat/Rocket.Chat/pull/9457) by [@jgtoriginal](https://github.com/jgtoriginal)) -- New sidebar layout ([#9608](https://github.com/RocketChat/Rocket.Chat/pull/9608)) +- New sidebar layout ([#9608](https://github.com/RocketChat/Rocket.Chat/pull/9608) by [@karlprieb](https://github.com/karlprieb)) - Message read receipts ([#9717](https://github.com/RocketChat/Rocket.Chat/pull/9717)) - Alert admins when user requires approval & alert users when the account is approved/activated/deactivated ([#7098](https://github.com/RocketChat/Rocket.Chat/pull/7098) by [@luisfn](https://github.com/luisfn)) - Allow configuration of SAML logout behavior ([#9527](https://github.com/RocketChat/Rocket.Chat/pull/9527)) @@ -1400,12 +3589,12 @@ - Makes shield icon configurable ([#9746](https://github.com/RocketChat/Rocket.Chat/pull/9746) by [@c0dzilla](https://github.com/c0dzilla)) - Global message search (beta: disabled by default) ([#9687](https://github.com/RocketChat/Rocket.Chat/pull/9687) by [@cyberhck](https://github.com/cyberhck) & [@savikko](https://github.com/savikko)) - Allow sounds when conversation is focused ([#9312](https://github.com/RocketChat/Rocket.Chat/pull/9312) by [@RationalCoding](https://github.com/RationalCoding)) -- API to fetch permissions & user roles ([#9519](https://github.com/RocketChat/Rocket.Chat/pull/9519)) -- REST API to use Spotlight ([#9509](https://github.com/RocketChat/Rocket.Chat/pull/9509)) +- API to fetch permissions & user roles ([#9519](https://github.com/RocketChat/Rocket.Chat/pull/9519) by [@rafaelks](https://github.com/rafaelks)) +- REST API to use Spotlight ([#9509](https://github.com/RocketChat/Rocket.Chat/pull/9509) by [@rafaelks](https://github.com/rafaelks)) - Option to proxy files and avatars through the server ([#9699](https://github.com/RocketChat/Rocket.Chat/pull/9699)) - Allow request avatar placeholders as PNG or JPG instead of SVG ([#8193](https://github.com/RocketChat/Rocket.Chat/pull/8193) by [@lindoelio](https://github.com/lindoelio)) - Image preview as 32x32 base64 jpeg ([#9218](https://github.com/RocketChat/Rocket.Chat/pull/9218) by [@jorgeluisrezende](https://github.com/jorgeluisrezende)) -- New REST API to mark channel as read ([#9507](https://github.com/RocketChat/Rocket.Chat/pull/9507)) +- New REST API to mark channel as read ([#9507](https://github.com/RocketChat/Rocket.Chat/pull/9507) by [@rafaelks](https://github.com/rafaelks)) - Add route to get user shield/badge ([#9549](https://github.com/RocketChat/Rocket.Chat/pull/9549) by [@kb0304](https://github.com/kb0304)) - GraphQL API ([#8158](https://github.com/RocketChat/Rocket.Chat/pull/8158) by [@kamilkisiela](https://github.com/kamilkisiela)) - Livestream tab ([#9255](https://github.com/RocketChat/Rocket.Chat/pull/9255) by [@gdelavald](https://github.com/gdelavald)) @@ -1415,8 +3604,8 @@ ### 🐛 Bug fixes - Typo on french translation for "Open" ([#9934](https://github.com/RocketChat/Rocket.Chat/pull/9934) by [@sizrar](https://github.com/sizrar)) -- Wrong behavior of rooms info's *Read Only* and *Collaborative* buttons ([#9665](https://github.com/RocketChat/Rocket.Chat/pull/9665)) -- Close button on file upload bar was not working ([#9662](https://github.com/RocketChat/Rocket.Chat/pull/9662)) +- Wrong behavior of rooms info's *Read Only* and *Collaborative* buttons ([#9665](https://github.com/RocketChat/Rocket.Chat/pull/9665) by [@karlprieb](https://github.com/karlprieb)) +- Close button on file upload bar was not working ([#9662](https://github.com/RocketChat/Rocket.Chat/pull/9662) by [@karlprieb](https://github.com/karlprieb)) - Chrome 64 breaks jitsi-meet iframe ([#9560](https://github.com/RocketChat/Rocket.Chat/pull/9560) by [@speedy01](https://github.com/speedy01)) - Harmonize channel-related actions ([#9697](https://github.com/RocketChat/Rocket.Chat/pull/9697)) - Custom emoji was cropping sometimes ([#9676](https://github.com/RocketChat/Rocket.Chat/pull/9676) by [@anu-007](https://github.com/anu-007)) @@ -1448,8 +3637,8 @@ 🔍 Minor changes - Release 0.62.0 ([#9935](https://github.com/RocketChat/Rocket.Chat/pull/9935)) -- Regression: Fix livechat queue link ([#9928](https://github.com/RocketChat/Rocket.Chat/pull/9928)) -- Regression: Directory now list default channel ([#9931](https://github.com/RocketChat/Rocket.Chat/pull/9931)) +- Regression: Fix livechat queue link ([#9928](https://github.com/RocketChat/Rocket.Chat/pull/9928) by [@karlprieb](https://github.com/karlprieb)) +- Regression: Directory now list default channel ([#9931](https://github.com/RocketChat/Rocket.Chat/pull/9931) by [@karlprieb](https://github.com/karlprieb)) - Improve link handling for attachments ([#9908](https://github.com/RocketChat/Rocket.Chat/pull/9908)) - Regression: Misplaced language dropdown in user preferences panel ([#9883](https://github.com/RocketChat/Rocket.Chat/pull/9883) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) - Fix RHCC image path for OpenShift and default to the current namespace. ([#9901](https://github.com/RocketChat/Rocket.Chat/pull/9901) by [@jsm84](https://github.com/jsm84)) @@ -1457,18 +3646,18 @@ - [OTHER] Rocket.Chat Apps ([#9666](https://github.com/RocketChat/Rocket.Chat/pull/9666)) - Move NRR package to inside the project and convert from CoffeeScript ([#9753](https://github.com/RocketChat/Rocket.Chat/pull/9753)) - Update to meteor 1.6.1 ([#9546](https://github.com/RocketChat/Rocket.Chat/pull/9546)) -- Regression: Avatar now open account related options ([#9843](https://github.com/RocketChat/Rocket.Chat/pull/9843)) -- Regression: Open search using ctrl/cmd + p and ctrl/cmd + k ([#9837](https://github.com/RocketChat/Rocket.Chat/pull/9837)) -- Regression: Search bar is now full width ([#9839](https://github.com/RocketChat/Rocket.Chat/pull/9839)) +- Regression: Avatar now open account related options ([#9843](https://github.com/RocketChat/Rocket.Chat/pull/9843) by [@karlprieb](https://github.com/karlprieb)) +- Regression: Open search using ctrl/cmd + p and ctrl/cmd + k ([#9837](https://github.com/RocketChat/Rocket.Chat/pull/9837) by [@karlprieb](https://github.com/karlprieb)) +- Regression: Search bar is now full width ([#9839](https://github.com/RocketChat/Rocket.Chat/pull/9839) by [@karlprieb](https://github.com/karlprieb)) - Dependencies update ([#9811](https://github.com/RocketChat/Rocket.Chat/pull/9811)) - Fix: Custom fields not showing on user info panel ([#9821](https://github.com/RocketChat/Rocket.Chat/pull/9821)) - Regression: Page was not respecting the window height on Firefox ([#9804](https://github.com/RocketChat/Rocket.Chat/pull/9804)) - Update bot-config.yml ([#9784](https://github.com/RocketChat/Rocket.Chat/pull/9784)) - Develop fix sync from master ([#9797](https://github.com/RocketChat/Rocket.Chat/pull/9797)) -- Regression: Change create channel icon ([#9851](https://github.com/RocketChat/Rocket.Chat/pull/9851)) -- Regression: Fix channel icons on safari ([#9852](https://github.com/RocketChat/Rocket.Chat/pull/9852)) -- Regression: Fix admin/user settings item text ([#9845](https://github.com/RocketChat/Rocket.Chat/pull/9845)) -- Regression: Improve sidebar filter ([#9905](https://github.com/RocketChat/Rocket.Chat/pull/9905)) +- Regression: Change create channel icon ([#9851](https://github.com/RocketChat/Rocket.Chat/pull/9851) by [@karlprieb](https://github.com/karlprieb)) +- Regression: Fix channel icons on safari ([#9852](https://github.com/RocketChat/Rocket.Chat/pull/9852) by [@karlprieb](https://github.com/karlprieb)) +- Regression: Fix admin/user settings item text ([#9845](https://github.com/RocketChat/Rocket.Chat/pull/9845) by [@karlprieb](https://github.com/karlprieb)) +- Regression: Improve sidebar filter ([#9905](https://github.com/RocketChat/Rocket.Chat/pull/9905) by [@karlprieb](https://github.com/karlprieb)) - [OTHER] Fix Apps not working on multi-instance deployments ([#9902](https://github.com/RocketChat/Rocket.Chat/pull/9902)) - [Fix] Not Translated Phrases ([#9877](https://github.com/RocketChat/Rocket.Chat/pull/9877) by [@bernardoetrevisan](https://github.com/bernardoetrevisan)) - Regression: Overlapping header in user profile panel ([#9889](https://github.com/RocketChat/Rocket.Chat/pull/9889) by [@kaiiiiiiiii](https://github.com/kaiiiiiiiii)) @@ -1492,11 +3681,13 @@ - [@jsm84](https://github.com/jsm84) - [@kaiiiiiiiii](https://github.com/kaiiiiiiiii) - [@kamilkisiela](https://github.com/kamilkisiela) +- [@karlprieb](https://github.com/karlprieb) - [@kb0304](https://github.com/kb0304) - [@kemitchell](https://github.com/kemitchell) - [@lindoelio](https://github.com/lindoelio) - [@luisfn](https://github.com/luisfn) - [@lunaticmonk](https://github.com/lunaticmonk) +- [@rafaelks](https://github.com/rafaelks) - [@ramrami](https://github.com/ramrami) - [@savikko](https://github.com/savikko) - [@sizrar](https://github.com/sizrar) @@ -1514,9 +3705,7 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@mrsimpson](https://github.com/mrsimpson) -- [@rafaelks](https://github.com/rafaelks) - [@renatobecker](https://github.com/renatobecker) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -1578,17 +3767,17 @@ ### 🎉 New features -- Contextual Bar Redesign ([#8411](https://github.com/RocketChat/Rocket.Chat/pull/8411)) +- Contextual Bar Redesign ([#8411](https://github.com/RocketChat/Rocket.Chat/pull/8411) by [@karlprieb](https://github.com/karlprieb)) - Update documentation: provide example for multiple basedn ([#9442](https://github.com/RocketChat/Rocket.Chat/pull/9442) by [@rndmh3ro](https://github.com/rndmh3ro)) -- Sidebar menu option to mark room as unread ([#9216](https://github.com/RocketChat/Rocket.Chat/pull/9216)) +- Sidebar menu option to mark room as unread ([#9216](https://github.com/RocketChat/Rocket.Chat/pull/9216) by [@karlprieb](https://github.com/karlprieb)) - Add mention-here permission #7631 ([#9228](https://github.com/RocketChat/Rocket.Chat/pull/9228) by [@ryjones](https://github.com/ryjones)) - Indicate the Self DM room ([#9234](https://github.com/RocketChat/Rocket.Chat/pull/9234)) -- new layout for emojipicker ([#9245](https://github.com/RocketChat/Rocket.Chat/pull/9245)) +- new layout for emojipicker ([#9245](https://github.com/RocketChat/Rocket.Chat/pull/9245) by [@karlprieb](https://github.com/karlprieb)) - add /home link to sidenav footer logo ([#9366](https://github.com/RocketChat/Rocket.Chat/pull/9366) by [@cyclops24](https://github.com/cyclops24)) - Livechat extract lead data from message ([#9135](https://github.com/RocketChat/Rocket.Chat/pull/9135)) - Add impersonate option for livechat triggers ([#9107](https://github.com/RocketChat/Rocket.Chat/pull/9107)) - Add support to external livechat queue service provider ([#9053](https://github.com/RocketChat/Rocket.Chat/pull/9053)) -- Contextual bar mail messages ([#9510](https://github.com/RocketChat/Rocket.Chat/pull/9510)) +- Contextual bar mail messages ([#9510](https://github.com/RocketChat/Rocket.Chat/pull/9510) by [@karlprieb](https://github.com/karlprieb)) ### 🐛 Bug fixes @@ -1601,7 +3790,7 @@ - Subscriptions not removed when removing user ([#9432](https://github.com/RocketChat/Rocket.Chat/pull/9432)) - Highlight setting not working correctly ([#9364](https://github.com/RocketChat/Rocket.Chat/pull/9364) by [@cyclops24](https://github.com/cyclops24)) - File access not working when passing credentials via querystring ([#9264](https://github.com/RocketChat/Rocket.Chat/pull/9264)) -- Contextual bar redesign ([#9481](https://github.com/RocketChat/Rocket.Chat/pull/9481) by [@gdelavald](https://github.com/gdelavald)) +- Contextual bar redesign ([#9481](https://github.com/RocketChat/Rocket.Chat/pull/9481) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) - mention-here is missing i18n text #9455 ([#9456](https://github.com/RocketChat/Rocket.Chat/pull/9456) by [@ryjones](https://github.com/ryjones)) - Fix livechat visitor edit ([#9506](https://github.com/RocketChat/Rocket.Chat/pull/9506)) - large names on userinfo, and admin user bug on users with no usernames ([#9493](https://github.com/RocketChat/Rocket.Chat/pull/9493) by [@gdelavald](https://github.com/gdelavald)) @@ -1609,12 +3798,12 @@
🔍 Minor changes -- Release 0.61.0 ([#9533](https://github.com/RocketChat/Rocket.Chat/pull/9533) by [@ryjones](https://github.com/ryjones)) +- Release 0.61.0 ([#9533](https://github.com/RocketChat/Rocket.Chat/pull/9533) by [@karlprieb](https://github.com/karlprieb) & [@ryjones](https://github.com/ryjones)) - Add community bot ([#9439](https://github.com/RocketChat/Rocket.Chat/pull/9439)) - Use correct version of Mailparser module ([#9356](https://github.com/RocketChat/Rocket.Chat/pull/9356)) -- Develop sync - Bump version to 0.61.0-develop ([#9260](https://github.com/RocketChat/Rocket.Chat/pull/9260) by [@cpitman](https://github.com/cpitman)) +- Develop sync - Bump version to 0.61.0-develop ([#9260](https://github.com/RocketChat/Rocket.Chat/pull/9260) by [@cpitman](https://github.com/cpitman) & [@karlprieb](https://github.com/karlprieb)) - [Fix] oauth not working because of email array ([#9173](https://github.com/RocketChat/Rocket.Chat/pull/9173)) -- [DOCS] Update the links of our Mobile Apps in Features topic ([#9469](https://github.com/RocketChat/Rocket.Chat/pull/9469)) +- [DOCS] Update the links of our Mobile Apps in Features topic ([#9469](https://github.com/RocketChat/Rocket.Chat/pull/9469) by [@rafaelks](https://github.com/rafaelks)) - Update license ([#9490](https://github.com/RocketChat/Rocket.Chat/pull/9490)) - Prevent NPM package-lock inside livechat ([#9504](https://github.com/RocketChat/Rocket.Chat/pull/9504)) @@ -1625,6 +3814,8 @@ - [@cpitman](https://github.com/cpitman) - [@cyclops24](https://github.com/cyclops24) - [@gdelavald](https://github.com/gdelavald) +- [@karlprieb](https://github.com/karlprieb) +- [@rafaelks](https://github.com/rafaelks) - [@ramrami](https://github.com/ramrami) - [@rndmh3ro](https://github.com/rndmh3ro) - [@ryjones](https://github.com/ryjones) @@ -1636,8 +3827,6 @@ - [@frdmn](https://github.com/frdmn) - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) -- [@karlprieb](https://github.com/karlprieb) -- [@rafaelks](https://github.com/rafaelks) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -1651,10 +3840,10 @@ ### 🐛 Bug fixes - LDAP TLS not working in some cases ([#9343](https://github.com/RocketChat/Rocket.Chat/pull/9343)) -- popover on safari for iOS ([#9328](https://github.com/RocketChat/Rocket.Chat/pull/9328)) -- announcement hyperlink color ([#9330](https://github.com/RocketChat/Rocket.Chat/pull/9330)) +- popover on safari for iOS ([#9328](https://github.com/RocketChat/Rocket.Chat/pull/9328) by [@karlprieb](https://github.com/karlprieb)) +- announcement hyperlink color ([#9330](https://github.com/RocketChat/Rocket.Chat/pull/9330) by [@karlprieb](https://github.com/karlprieb)) - Deleting message with store last message not removing ([#9335](https://github.com/RocketChat/Rocket.Chat/pull/9335)) -- last message cutting on bottom ([#9345](https://github.com/RocketChat/Rocket.Chat/pull/9345)) +- last message cutting on bottom ([#9345](https://github.com/RocketChat/Rocket.Chat/pull/9345) by [@karlprieb](https://github.com/karlprieb))
🔍 Minor changes @@ -1664,9 +3853,12 @@
-### 👩‍💻👨‍💻 Core Team 🤓 +### 👩‍💻👨‍💻 Contributors 😍 - [@karlprieb](https://github.com/karlprieb) + +### 👩‍💻👨‍💻 Core Team 🤓 + - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -1679,9 +3871,9 @@ ### 🐛 Bug fixes -- custom emoji size on sidebar item ([#9314](https://github.com/RocketChat/Rocket.Chat/pull/9314)) -- svg render on firefox ([#9311](https://github.com/RocketChat/Rocket.Chat/pull/9311)) -- sidebar footer padding ([#9249](https://github.com/RocketChat/Rocket.Chat/pull/9249)) +- custom emoji size on sidebar item ([#9314](https://github.com/RocketChat/Rocket.Chat/pull/9314) by [@karlprieb](https://github.com/karlprieb)) +- svg render on firefox ([#9311](https://github.com/RocketChat/Rocket.Chat/pull/9311) by [@karlprieb](https://github.com/karlprieb)) +- sidebar footer padding ([#9249](https://github.com/RocketChat/Rocket.Chat/pull/9249) by [@karlprieb](https://github.com/karlprieb)) - LDAP/AD is not importing all users ([#9309](https://github.com/RocketChat/Rocket.Chat/pull/9309)) - Wrong position of notifications alert in accounts preference page ([#9289](https://github.com/RocketChat/Rocket.Chat/pull/9289) by [@HammyHavoc](https://github.com/HammyHavoc)) - English Typos ([#9285](https://github.com/RocketChat/Rocket.Chat/pull/9285) by [@HammyHavoc](https://github.com/HammyHavoc)) @@ -1700,10 +3892,10 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@HammyHavoc](https://github.com/HammyHavoc) +- [@karlprieb](https://github.com/karlprieb) ### 👩‍💻👨‍💻 Core Team 🤓 -- [@karlprieb](https://github.com/karlprieb) - [@rodrigok](https://github.com/rodrigok) # 0.60.2 @@ -1756,7 +3948,7 @@ ### 🎉 New features - Allow user's default preferences configuration ([#7285](https://github.com/RocketChat/Rocket.Chat/pull/7285) by [@goiaba](https://github.com/goiaba)) -- Add "Favorites" and "Mark as read" options to the room list ([#8915](https://github.com/RocketChat/Rocket.Chat/pull/8915)) +- Add "Favorites" and "Mark as read" options to the room list ([#8915](https://github.com/RocketChat/Rocket.Chat/pull/8915) by [@karlprieb](https://github.com/karlprieb)) - Facebook livechat integration ([#8807](https://github.com/RocketChat/Rocket.Chat/pull/8807)) - Added support for Dataporten's userid-feide scope ([#8902](https://github.com/RocketChat/Rocket.Chat/pull/8902) by [@torgeirl](https://github.com/torgeirl)) - Describe file uploads when notifying by email ([#8924](https://github.com/RocketChat/Rocket.Chat/pull/8924)) @@ -1767,27 +3959,27 @@ - Adds admin option to globally set mobile devices to always be notified regardless of presence status. ([#7641](https://github.com/RocketChat/Rocket.Chat/pull/7641) by [@stalley](https://github.com/stalley)) - Add new API endpoints ([#8947](https://github.com/RocketChat/Rocket.Chat/pull/8947)) - Option to enable/disable auto away and configure timer ([#8029](https://github.com/RocketChat/Rocket.Chat/pull/8029) by [@armand1m](https://github.com/armand1m)) -- New Modal component ([#8882](https://github.com/RocketChat/Rocket.Chat/pull/8882)) +- New Modal component ([#8882](https://github.com/RocketChat/Rocket.Chat/pull/8882) by [@karlprieb](https://github.com/karlprieb)) - Improve room types API and usages ([#9009](https://github.com/RocketChat/Rocket.Chat/pull/9009)) -- Room counter sidebar preference ([#8866](https://github.com/RocketChat/Rocket.Chat/pull/8866)) -- Save room's last message ([#8979](https://github.com/RocketChat/Rocket.Chat/pull/8979)) -- Token Controlled Access channels ([#8060](https://github.com/RocketChat/Rocket.Chat/pull/8060) by [@lindoelio](https://github.com/lindoelio)) +- Room counter sidebar preference ([#8866](https://github.com/RocketChat/Rocket.Chat/pull/8866) by [@karlprieb](https://github.com/karlprieb)) +- Save room's last message ([#8979](https://github.com/RocketChat/Rocket.Chat/pull/8979) by [@karlprieb](https://github.com/karlprieb)) +- Token Controlled Access channels ([#8060](https://github.com/RocketChat/Rocket.Chat/pull/8060) by [@karlprieb](https://github.com/karlprieb) & [@lindoelio](https://github.com/lindoelio)) - Send category and title fields to iOS push notification ([#8905](https://github.com/RocketChat/Rocket.Chat/pull/8905)) - code to get the updated messages ([#8857](https://github.com/RocketChat/Rocket.Chat/pull/8857)) - Rest API endpoints to list, get, and run commands ([#8531](https://github.com/RocketChat/Rocket.Chat/pull/8531)) -- Upgrade Meteor to 1.6 ([#8715](https://github.com/RocketChat/Rocket.Chat/pull/8715)) +- Upgrade Meteor to 1.6 ([#8715](https://github.com/RocketChat/Rocket.Chat/pull/8715) by [@karlprieb](https://github.com/karlprieb)) - Add settings for allow user direct messages to yourself ([#8066](https://github.com/RocketChat/Rocket.Chat/pull/8066) by [@lindoelio](https://github.com/lindoelio)) - Add sweet alert to video call tab ([#8108](https://github.com/RocketChat/Rocket.Chat/pull/8108)) - Displays QR code for manually entering when enabling 2fa ([#8143](https://github.com/RocketChat/Rocket.Chat/pull/8143)) - Add yunohost.org installation method to Readme.md ([#8037](https://github.com/RocketChat/Rocket.Chat/pull/8037) by [@selamanse](https://github.com/selamanse)) -- Modal ([#9092](https://github.com/RocketChat/Rocket.Chat/pull/9092)) +- Modal ([#9092](https://github.com/RocketChat/Rocket.Chat/pull/9092) by [@karlprieb](https://github.com/karlprieb)) - Make Custom oauth accept nested usernameField ([#9066](https://github.com/RocketChat/Rocket.Chat/pull/9066)) ### 🐛 Bug fixes -- Can't react on Read Only rooms even when enabled ([#8925](https://github.com/RocketChat/Rocket.Chat/pull/8925)) +- Can't react on Read Only rooms even when enabled ([#8925](https://github.com/RocketChat/Rocket.Chat/pull/8925) by [@karlprieb](https://github.com/karlprieb)) - CAS does not share secrets when operating multiple server instances ([#8654](https://github.com/RocketChat/Rocket.Chat/pull/8654) by [@AmShaegar13](https://github.com/AmShaegar13)) -- Snippetted messages not working ([#8937](https://github.com/RocketChat/Rocket.Chat/pull/8937)) +- Snippetted messages not working ([#8937](https://github.com/RocketChat/Rocket.Chat/pull/8937) by [@karlprieb](https://github.com/karlprieb)) - Added afterUserCreated trigger after first CAS login ([#9022](https://github.com/RocketChat/Rocket.Chat/pull/9022) by [@AmShaegar13](https://github.com/AmShaegar13)) - Notification is not sent when a video conference start ([#8828](https://github.com/RocketChat/Rocket.Chat/pull/8828) by [@deepseainside75](https://github.com/deepseainside75) & [@stefanoverducci](https://github.com/stefanoverducci)) - long filename overlaps cancel button in progress bar ([#8868](https://github.com/RocketChat/Rocket.Chat/pull/8868) by [@joesitton](https://github.com/joesitton)) @@ -1804,27 +3996,27 @@ - Sync of non existent field throws exception ([#8006](https://github.com/RocketChat/Rocket.Chat/pull/8006) by [@goiaba](https://github.com/goiaba)) - Autoupdate of CSS does not work when using a prefix ([#8107](https://github.com/RocketChat/Rocket.Chat/pull/8107) by [@Darkneon](https://github.com/Darkneon)) - Contextual errors for this and RegExp declarations in IRC module ([#8656](https://github.com/RocketChat/Rocket.Chat/pull/8656) by [@Pharserror](https://github.com/Pharserror)) -- Wrong room counter name ([#9013](https://github.com/RocketChat/Rocket.Chat/pull/9013)) -- Message-box autogrow flick ([#8932](https://github.com/RocketChat/Rocket.Chat/pull/8932)) +- Wrong room counter name ([#9013](https://github.com/RocketChat/Rocket.Chat/pull/9013) by [@karlprieb](https://github.com/karlprieb)) +- Message-box autogrow flick ([#8932](https://github.com/RocketChat/Rocket.Chat/pull/8932) by [@karlprieb](https://github.com/karlprieb)) - Don't strip trailing slash on autolinker urls ([#8812](https://github.com/RocketChat/Rocket.Chat/pull/8812) by [@jwilkins](https://github.com/jwilkins)) -- Change the unread messages style ([#8883](https://github.com/RocketChat/Rocket.Chat/pull/8883)) -- Missing sidebar footer padding ([#8884](https://github.com/RocketChat/Rocket.Chat/pull/8884)) -- Long room announcement cut off ([#8907](https://github.com/RocketChat/Rocket.Chat/pull/8907)) +- Change the unread messages style ([#8883](https://github.com/RocketChat/Rocket.Chat/pull/8883) by [@karlprieb](https://github.com/karlprieb)) +- Missing sidebar footer padding ([#8884](https://github.com/RocketChat/Rocket.Chat/pull/8884) by [@karlprieb](https://github.com/karlprieb)) +- Long room announcement cut off ([#8907](https://github.com/RocketChat/Rocket.Chat/pull/8907) by [@karlprieb](https://github.com/karlprieb)) - DM email notifications always being sent regardless of account setting ([#8917](https://github.com/RocketChat/Rocket.Chat/pull/8917) by [@ashward](https://github.com/ashward)) - Typo Fix ([#8938](https://github.com/RocketChat/Rocket.Chat/pull/8938) by [@seangeleno](https://github.com/seangeleno)) - Katex markdown link changed ([#8948](https://github.com/RocketChat/Rocket.Chat/pull/8948) by [@mritunjaygoutam12](https://github.com/mritunjaygoutam12)) - if ogImage exists use it over image in oembedUrlWidget ([#9000](https://github.com/RocketChat/Rocket.Chat/pull/9000) by [@satyapramodh](https://github.com/satyapramodh)) - Cannot edit or delete custom sounds ([#8889](https://github.com/RocketChat/Rocket.Chat/pull/8889) by [@ccfang](https://github.com/ccfang)) - Change old 'rocketbot' username to 'InternalHubot_Username' setting ([#8928](https://github.com/RocketChat/Rocket.Chat/pull/8928) by [@ramrami](https://github.com/ramrami)) -- Link for channels are not rendering correctly ([#8985](https://github.com/RocketChat/Rocket.Chat/pull/8985)) +- Link for channels are not rendering correctly ([#8985](https://github.com/RocketChat/Rocket.Chat/pull/8985) by [@karlprieb](https://github.com/karlprieb)) - Xenforo [BD]API for 'user.user_id; instead of 'id' ([#8968](https://github.com/RocketChat/Rocket.Chat/pull/8968) by [@wesnspace](https://github.com/wesnspace)) -- flextab height on smaller screens ([#8994](https://github.com/RocketChat/Rocket.Chat/pull/8994)) +- flextab height on smaller screens ([#8994](https://github.com/RocketChat/Rocket.Chat/pull/8994) by [@karlprieb](https://github.com/karlprieb)) - Check for mention-all permission in room scope ([#8931](https://github.com/RocketChat/Rocket.Chat/pull/8931)) - fix emoji package path so they show up correctly in browser ([#8822](https://github.com/RocketChat/Rocket.Chat/pull/8822) by [@ryoshimizu](https://github.com/ryoshimizu)) - Set correct Twitter link ([#8830](https://github.com/RocketChat/Rocket.Chat/pull/8830) by [@jotafeldmann](https://github.com/jotafeldmann)) -- User email settings on DM ([#8810](https://github.com/RocketChat/Rocket.Chat/pull/8810)) +- User email settings on DM ([#8810](https://github.com/RocketChat/Rocket.Chat/pull/8810) by [@karlprieb](https://github.com/karlprieb)) - i18n'd Resend_verification_mail, username_initials, upload avatar ([#8721](https://github.com/RocketChat/Rocket.Chat/pull/8721) by [@arungalva](https://github.com/arungalva)) -- Username clipping on firefox ([#8716](https://github.com/RocketChat/Rocket.Chat/pull/8716)) +- Username clipping on firefox ([#8716](https://github.com/RocketChat/Rocket.Chat/pull/8716) by [@karlprieb](https://github.com/karlprieb)) - Improved grammar and made it clearer to the user ([#8795](https://github.com/RocketChat/Rocket.Chat/pull/8795) by [@HammyHavoc](https://github.com/HammyHavoc)) - Show real name of current user at top of side nav if setting enabled ([#8718](https://github.com/RocketChat/Rocket.Chat/pull/8718)) - Range Slider Value label has bug in RTL ([#8441](https://github.com/RocketChat/Rocket.Chat/pull/8441) by [@cyclops24](https://github.com/cyclops24)) @@ -1835,30 +4027,30 @@ - Changed all rocket.chat/docs/ to docs.rocket.chat/ ([#8588](https://github.com/RocketChat/Rocket.Chat/pull/8588) by [@RekkyRek](https://github.com/RekkyRek)) - Email verification indicator added ([#7923](https://github.com/RocketChat/Rocket.Chat/pull/7923) by [@aditya19496](https://github.com/aditya19496)) - REST API file upload not respecting size limit ([#9108](https://github.com/RocketChat/Rocket.Chat/pull/9108)) -- Creating channels on Firefox ([#9109](https://github.com/RocketChat/Rocket.Chat/pull/9109)) -- Some UI problems on 0.60 ([#9095](https://github.com/RocketChat/Rocket.Chat/pull/9095)) +- Creating channels on Firefox ([#9109](https://github.com/RocketChat/Rocket.Chat/pull/9109) by [@karlprieb](https://github.com/karlprieb)) +- Some UI problems on 0.60 ([#9095](https://github.com/RocketChat/Rocket.Chat/pull/9095) by [@karlprieb](https://github.com/karlprieb)) - Update rocketchat:streamer to be compatible with previous version ([#9094](https://github.com/RocketChat/Rocket.Chat/pull/9094)) - Importers not recovering when an error occurs ([#9134](https://github.com/RocketChat/Rocket.Chat/pull/9134)) - Do not block room while loading history ([#9121](https://github.com/RocketChat/Rocket.Chat/pull/9121)) - Channel page error ([#9091](https://github.com/RocketChat/Rocket.Chat/pull/9091) by [@ggrish](https://github.com/ggrish)) - Update Rocket.Chat for sandstorm ([#9062](https://github.com/RocketChat/Rocket.Chat/pull/9062) by [@peterlee0127](https://github.com/peterlee0127)) -- modal data on enter and modal style for file preview ([#9171](https://github.com/RocketChat/Rocket.Chat/pull/9171)) -- show oauth logins when adblock is used ([#9170](https://github.com/RocketChat/Rocket.Chat/pull/9170)) +- modal data on enter and modal style for file preview ([#9171](https://github.com/RocketChat/Rocket.Chat/pull/9171) by [@karlprieb](https://github.com/karlprieb)) +- show oauth logins when adblock is used ([#9170](https://github.com/RocketChat/Rocket.Chat/pull/9170) by [@karlprieb](https://github.com/karlprieb)) - Last sent message reoccurs in textbox ([#9169](https://github.com/RocketChat/Rocket.Chat/pull/9169)) - Made welcome emails more readable ([#9193](https://github.com/RocketChat/Rocket.Chat/pull/9193) by [@HammyHavoc](https://github.com/HammyHavoc)) -- Unread bar position when room have announcement ([#9188](https://github.com/RocketChat/Rocket.Chat/pull/9188)) -- Emoji size on last message preview ([#9186](https://github.com/RocketChat/Rocket.Chat/pull/9186)) -- Cursor position when reply on safari ([#9185](https://github.com/RocketChat/Rocket.Chat/pull/9185)) -- "Use Emoji" preference not working ([#9182](https://github.com/RocketChat/Rocket.Chat/pull/9182)) -- make the cross icon on user selection at channel creation page work ([#9176](https://github.com/RocketChat/Rocket.Chat/pull/9176) by [@vitor-nagao](https://github.com/vitor-nagao)) -- go to replied message ([#9172](https://github.com/RocketChat/Rocket.Chat/pull/9172)) -- channel create scroll on small screens ([#9168](https://github.com/RocketChat/Rocket.Chat/pull/9168)) +- Unread bar position when room have announcement ([#9188](https://github.com/RocketChat/Rocket.Chat/pull/9188) by [@karlprieb](https://github.com/karlprieb)) +- Emoji size on last message preview ([#9186](https://github.com/RocketChat/Rocket.Chat/pull/9186) by [@karlprieb](https://github.com/karlprieb)) +- Cursor position when reply on safari ([#9185](https://github.com/RocketChat/Rocket.Chat/pull/9185) by [@karlprieb](https://github.com/karlprieb)) +- "Use Emoji" preference not working ([#9182](https://github.com/RocketChat/Rocket.Chat/pull/9182) by [@karlprieb](https://github.com/karlprieb)) +- make the cross icon on user selection at channel creation page work ([#9176](https://github.com/RocketChat/Rocket.Chat/pull/9176) by [@karlprieb](https://github.com/karlprieb) & [@vitor-nagao](https://github.com/vitor-nagao)) +- go to replied message ([#9172](https://github.com/RocketChat/Rocket.Chat/pull/9172) by [@karlprieb](https://github.com/karlprieb)) +- channel create scroll on small screens ([#9168](https://github.com/RocketChat/Rocket.Chat/pull/9168) by [@karlprieb](https://github.com/karlprieb)) - Error when user roles is missing or is invalid ([#9040](https://github.com/RocketChat/Rocket.Chat/pull/9040) by [@paulovitin](https://github.com/paulovitin)) -- Make mentions and menu icons color darker ([#8922](https://github.com/RocketChat/Rocket.Chat/pull/8922)) -- Show modal with announcement ([#9241](https://github.com/RocketChat/Rocket.Chat/pull/9241)) -- File upload not working on IE and weird on Chrome ([#9206](https://github.com/RocketChat/Rocket.Chat/pull/9206)) +- Make mentions and menu icons color darker ([#8922](https://github.com/RocketChat/Rocket.Chat/pull/8922) by [@karlprieb](https://github.com/karlprieb)) +- Show modal with announcement ([#9241](https://github.com/RocketChat/Rocket.Chat/pull/9241) by [@karlprieb](https://github.com/karlprieb)) +- File upload not working on IE and weird on Chrome ([#9206](https://github.com/RocketChat/Rocket.Chat/pull/9206) by [@karlprieb](https://github.com/karlprieb)) - "Enter usernames" placeholder is cutting in "create channel" view ([#9194](https://github.com/RocketChat/Rocket.Chat/pull/9194) by [@TheReal1604](https://github.com/TheReal1604)) -- Move emojipicker css to theme package ([#9243](https://github.com/RocketChat/Rocket.Chat/pull/9243)) +- Move emojipicker css to theme package ([#9243](https://github.com/RocketChat/Rocket.Chat/pull/9243) by [@karlprieb](https://github.com/karlprieb))
🔍 Minor changes @@ -1878,7 +4070,7 @@ - Changed wording for "Maximum Allowed Message Size" ([#8872](https://github.com/RocketChat/Rocket.Chat/pull/8872) by [@HammyHavoc](https://github.com/HammyHavoc)) - Fix Docker image build ([#8862](https://github.com/RocketChat/Rocket.Chat/pull/8862)) - Fix link to .asc file on S3 ([#8829](https://github.com/RocketChat/Rocket.Chat/pull/8829)) -- Bump version to 0.60.0-develop ([#8820](https://github.com/RocketChat/Rocket.Chat/pull/8820) by [@gdelavald](https://github.com/gdelavald)) +- Bump version to 0.60.0-develop ([#8820](https://github.com/RocketChat/Rocket.Chat/pull/8820) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) - Update path for s3 redirect in circle ci ([#8819](https://github.com/RocketChat/Rocket.Chat/pull/8819)) - Remove chatops package ([#8742](https://github.com/RocketChat/Rocket.Chat/pull/8742)) - Removed tmeasday:crypto-md5 ([#8743](https://github.com/RocketChat/Rocket.Chat/pull/8743)) @@ -1923,7 +4115,7 @@ - Dependencies Update ([#9197](https://github.com/RocketChat/Rocket.Chat/pull/9197)) - Fix: Rooms and users are using different avatar style ([#9196](https://github.com/RocketChat/Rocket.Chat/pull/9196)) - Typo: German language file ([#9190](https://github.com/RocketChat/Rocket.Chat/pull/9190) by [@TheReal1604](https://github.com/TheReal1604)) -- Fix: Snippet name to not showing in snippet list ([#9184](https://github.com/RocketChat/Rocket.Chat/pull/9184)) +- Fix: Snippet name to not showing in snippet list ([#9184](https://github.com/RocketChat/Rocket.Chat/pull/9184) by [@karlprieb](https://github.com/karlprieb)) - Fix/api me only return verified ([#9183](https://github.com/RocketChat/Rocket.Chat/pull/9183)) - Fix: UI: Descenders of glyphs are cut off ([#9181](https://github.com/RocketChat/Rocket.Chat/pull/9181)) - Fix: Unneeded warning in payload of REST API calls ([#9240](https://github.com/RocketChat/Rocket.Chat/pull/9240)) @@ -1934,7 +4126,7 @@ - Do not change room icon color when room is unread ([#9257](https://github.com/RocketChat/Rocket.Chat/pull/9257)) - LingoHub based on develop ([#9256](https://github.com/RocketChat/Rocket.Chat/pull/9256)) - Add curl, its missing on worker nodes so has to be explicitly added ([#9248](https://github.com/RocketChat/Rocket.Chat/pull/9248)) -- Fix: Sidebar item on rtl and small devices ([#9247](https://github.com/RocketChat/Rocket.Chat/pull/9247)) +- Fix: Sidebar item on rtl and small devices ([#9247](https://github.com/RocketChat/Rocket.Chat/pull/9247) by [@karlprieb](https://github.com/karlprieb))
@@ -1965,6 +4157,7 @@ - [@joesitton](https://github.com/joesitton) - [@jotafeldmann](https://github.com/jotafeldmann) - [@jwilkins](https://github.com/jwilkins) +- [@karlprieb](https://github.com/karlprieb) - [@lindoelio](https://github.com/lindoelio) - [@mastappl](https://github.com/mastappl) - [@mritunjaygoutam12](https://github.com/mritunjaygoutam12) @@ -1996,7 +4189,6 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@marceloschmidt](https://github.com/marceloschmidt) - [@mrsimpson](https://github.com/mrsimpson) - [@pierreozoux](https://github.com/pierreozoux) @@ -2048,12 +4240,12 @@ ### 🐛 Bug fixes -- Channel settings buttons ([#8753](https://github.com/RocketChat/Rocket.Chat/pull/8753)) +- Channel settings buttons ([#8753](https://github.com/RocketChat/Rocket.Chat/pull/8753) by [@karlprieb](https://github.com/karlprieb))
🔍 Minor changes -- Release/0.59.4 ([#8967](https://github.com/RocketChat/Rocket.Chat/pull/8967) by [@cpitman](https://github.com/cpitman)) +- Release/0.59.4 ([#8967](https://github.com/RocketChat/Rocket.Chat/pull/8967) by [@cpitman](https://github.com/cpitman) & [@karlprieb](https://github.com/karlprieb)) - Add CircleCI ([#8685](https://github.com/RocketChat/Rocket.Chat/pull/8685))
@@ -2061,11 +4253,11 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@cpitman](https://github.com/cpitman) +- [@karlprieb](https://github.com/karlprieb) ### 👩‍💻👨‍💻 Core Team 🤓 - [@geekgonecrazy](https://github.com/geekgonecrazy) -- [@karlprieb](https://github.com/karlprieb) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -2080,7 +4272,7 @@ - AmazonS3: Quote file.name for ContentDisposition for files with commas ([#8593](https://github.com/RocketChat/Rocket.Chat/pull/8593)) - Fix e-mail message forward ([#8645](https://github.com/RocketChat/Rocket.Chat/pull/8645)) -- Audio message icon ([#8648](https://github.com/RocketChat/Rocket.Chat/pull/8648)) +- Audio message icon ([#8648](https://github.com/RocketChat/Rocket.Chat/pull/8648) by [@karlprieb](https://github.com/karlprieb)) - Highlighted color height issue ([#8431](https://github.com/RocketChat/Rocket.Chat/pull/8431) by [@cyclops24](https://github.com/cyclops24)) - Update pt-BR translation ([#8655](https://github.com/RocketChat/Rocket.Chat/pull/8655) by [@rodorgas](https://github.com/rodorgas)) - Fix typos ([#8679](https://github.com/RocketChat/Rocket.Chat/pull/8679)) @@ -2097,13 +4289,13 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@cyclops24](https://github.com/cyclops24) +- [@karlprieb](https://github.com/karlprieb) - [@rodorgas](https://github.com/rodorgas) - [@vikaskedia](https://github.com/vikaskedia) ### 👩‍💻👨‍💻 Core Team 🤓 - [@geekgonecrazy](https://github.com/geekgonecrazy) -- [@karlprieb](https://github.com/karlprieb) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) - [@xenithorb](https://github.com/xenithorb) @@ -2117,8 +4309,8 @@ ### 🐛 Bug fixes -- Missing scroll at create channel page ([#8637](https://github.com/RocketChat/Rocket.Chat/pull/8637)) -- Message popup menu on mobile/cordova ([#8634](https://github.com/RocketChat/Rocket.Chat/pull/8634)) +- Missing scroll at create channel page ([#8637](https://github.com/RocketChat/Rocket.Chat/pull/8637) by [@karlprieb](https://github.com/karlprieb)) +- Message popup menu on mobile/cordova ([#8634](https://github.com/RocketChat/Rocket.Chat/pull/8634) by [@karlprieb](https://github.com/karlprieb)) - API channel/group.members not sorting ([#8635](https://github.com/RocketChat/Rocket.Chat/pull/8635)) - LDAP not merging existent users && Wrong id link generation ([#8613](https://github.com/RocketChat/Rocket.Chat/pull/8613)) - encode filename in url to prevent links breaking ([#8551](https://github.com/RocketChat/Rocket.Chat/pull/8551) by [@joesitton](https://github.com/joesitton)) @@ -2127,10 +4319,10 @@ ### 👩‍💻👨‍💻 Contributors 😍 - [@joesitton](https://github.com/joesitton) +- [@karlprieb](https://github.com/karlprieb) ### 👩‍💻👨‍💻 Core Team 🤓 -- [@karlprieb](https://github.com/karlprieb) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -2162,10 +4354,10 @@ ### 🎉 New features -- Replace message cog for vertical menu ([#7864](https://github.com/RocketChat/Rocket.Chat/pull/7864)) +- Replace message cog for vertical menu ([#7864](https://github.com/RocketChat/Rocket.Chat/pull/7864) by [@karlprieb](https://github.com/karlprieb)) - block users to mention unknow users ([#7830](https://github.com/RocketChat/Rocket.Chat/pull/7830)) - Allow ldap mapping of customFields ([#7614](https://github.com/RocketChat/Rocket.Chat/pull/7614) by [@goiaba](https://github.com/goiaba)) -- Create a standard for our svg icons ([#7853](https://github.com/RocketChat/Rocket.Chat/pull/7853)) +- Create a standard for our svg icons ([#7853](https://github.com/RocketChat/Rocket.Chat/pull/7853) by [@karlprieb](https://github.com/karlprieb)) - Allows admin to list all groups with API ([#7565](https://github.com/RocketChat/Rocket.Chat/pull/7565) by [@mboudet](https://github.com/mboudet)) - Add markdown parser "marked" ([#7852](https://github.com/RocketChat/Rocket.Chat/pull/7852) by [@nishimaki10](https://github.com/nishimaki10)) - Audio Notification updated in sidebar ([#7817](https://github.com/RocketChat/Rocket.Chat/pull/7817) by [@aditya19496](https://github.com/aditya19496) & [@maarten-v](https://github.com/maarten-v)) @@ -2173,18 +4365,18 @@ - Template to show Custom Fields in user info view ([#7688](https://github.com/RocketChat/Rocket.Chat/pull/7688) by [@goiaba](https://github.com/goiaba)) - Add room type as a class to the ul-group of rooms ([#7711](https://github.com/RocketChat/Rocket.Chat/pull/7711) by [@danischreiber](https://github.com/danischreiber)) - Add classes to notification menu so they can be hidden in css ([#7636](https://github.com/RocketChat/Rocket.Chat/pull/7636) by [@danischreiber](https://github.com/danischreiber)) -- Adds a Keyboard Shortcut option to the flextab ([#5902](https://github.com/RocketChat/Rocket.Chat/pull/5902) by [@cnash](https://github.com/cnash)) +- Adds a Keyboard Shortcut option to the flextab ([#5902](https://github.com/RocketChat/Rocket.Chat/pull/5902) by [@cnash](https://github.com/cnash) & [@karlprieb](https://github.com/karlprieb)) - Integrated personal email gateway (GSoC'17) ([#7342](https://github.com/RocketChat/Rocket.Chat/pull/7342) by [@pkgodara](https://github.com/pkgodara)) -- Add tags to uploaded images using Google Cloud Vision API ([#6301](https://github.com/RocketChat/Rocket.Chat/pull/6301)) +- Add tags to uploaded images using Google Cloud Vision API ([#6301](https://github.com/RocketChat/Rocket.Chat/pull/6301) by [@karlprieb](https://github.com/karlprieb)) - Package to render issue numbers into links to an issue tracker. ([#6700](https://github.com/RocketChat/Rocket.Chat/pull/6700) by [@TAdeJong](https://github.com/TAdeJong) & [@TobiasKappe](https://github.com/TobiasKappe)) - Automatically select the first channel ([#7350](https://github.com/RocketChat/Rocket.Chat/pull/7350) by [@antaryami-sahoo](https://github.com/antaryami-sahoo)) - Rocket.Chat UI Redesign ([#7643](https://github.com/RocketChat/Rocket.Chat/pull/7643)) - Upgrade to meteor 1.5.2 ([#8073](https://github.com/RocketChat/Rocket.Chat/pull/8073)) -- Enable read only channel creation ([#8260](https://github.com/RocketChat/Rocket.Chat/pull/8260)) +- Enable read only channel creation ([#8260](https://github.com/RocketChat/Rocket.Chat/pull/8260) by [@karlprieb](https://github.com/karlprieb)) - Add RD Station integration to livechat ([#8304](https://github.com/RocketChat/Rocket.Chat/pull/8304)) -- Unify unread and mentions badge ([#8361](https://github.com/RocketChat/Rocket.Chat/pull/8361)) -- make sidebar item width 100% ([#8362](https://github.com/RocketChat/Rocket.Chat/pull/8362)) -- Smaller accountBox ([#8360](https://github.com/RocketChat/Rocket.Chat/pull/8360)) +- Unify unread and mentions badge ([#8361](https://github.com/RocketChat/Rocket.Chat/pull/8361) by [@karlprieb](https://github.com/karlprieb)) +- make sidebar item width 100% ([#8362](https://github.com/RocketChat/Rocket.Chat/pull/8362) by [@karlprieb](https://github.com/karlprieb)) +- Smaller accountBox ([#8360](https://github.com/RocketChat/Rocket.Chat/pull/8360) by [@karlprieb](https://github.com/karlprieb)) - Setting to disable MarkDown and enable AutoLinker ([#8459](https://github.com/RocketChat/Rocket.Chat/pull/8459)) ### 🐛 Bug fixes @@ -2208,77 +4400,77 @@ - Make link inside YouTube preview open in new tab ([#7679](https://github.com/RocketChat/Rocket.Chat/pull/7679) by [@1lann](https://github.com/1lann)) - Remove references to non-existent tests ([#7672](https://github.com/RocketChat/Rocket.Chat/pull/7672) by [@Kiran-Rao](https://github.com/Kiran-Rao)) - Example usage of unsubscribe.js ([#7673](https://github.com/RocketChat/Rocket.Chat/pull/7673) by [@Kiran-Rao](https://github.com/Kiran-Rao)) -- sidebar paddings ([#7880](https://github.com/RocketChat/Rocket.Chat/pull/7880)) +- sidebar paddings ([#7880](https://github.com/RocketChat/Rocket.Chat/pull/7880) by [@karlprieb](https://github.com/karlprieb)) - Adds default search text padding for emoji search ([#7878](https://github.com/RocketChat/Rocket.Chat/pull/7878) by [@gdelavald](https://github.com/gdelavald)) -- search results position on sidebar ([#7881](https://github.com/RocketChat/Rocket.Chat/pull/7881)) -- hyperlink style on sidebar footer ([#7882](https://github.com/RocketChat/Rocket.Chat/pull/7882)) -- popover position on mobile ([#7883](https://github.com/RocketChat/Rocket.Chat/pull/7883)) -- message actions over unread bar ([#7885](https://github.com/RocketChat/Rocket.Chat/pull/7885)) -- livechat icon ([#7886](https://github.com/RocketChat/Rocket.Chat/pull/7886)) +- search results position on sidebar ([#7881](https://github.com/RocketChat/Rocket.Chat/pull/7881) by [@karlprieb](https://github.com/karlprieb)) +- hyperlink style on sidebar footer ([#7882](https://github.com/RocketChat/Rocket.Chat/pull/7882) by [@karlprieb](https://github.com/karlprieb)) +- popover position on mobile ([#7883](https://github.com/RocketChat/Rocket.Chat/pull/7883) by [@karlprieb](https://github.com/karlprieb)) +- message actions over unread bar ([#7885](https://github.com/RocketChat/Rocket.Chat/pull/7885) by [@karlprieb](https://github.com/karlprieb)) +- livechat icon ([#7886](https://github.com/RocketChat/Rocket.Chat/pull/7886) by [@karlprieb](https://github.com/karlprieb)) - Makes text action menu width based on content size ([#7887](https://github.com/RocketChat/Rocket.Chat/pull/7887) by [@gdelavald](https://github.com/gdelavald)) -- sidebar buttons and badge paddings ([#7888](https://github.com/RocketChat/Rocket.Chat/pull/7888)) +- sidebar buttons and badge paddings ([#7888](https://github.com/RocketChat/Rocket.Chat/pull/7888) by [@karlprieb](https://github.com/karlprieb)) - Fix google play logo on repo README ([#7912](https://github.com/RocketChat/Rocket.Chat/pull/7912) by [@luizbills](https://github.com/luizbills)) - Fix livechat toggle UI issue ([#7904](https://github.com/RocketChat/Rocket.Chat/pull/7904)) - Remove break change in Realtime API ([#7895](https://github.com/RocketChat/Rocket.Chat/pull/7895)) - Window exception when parsing Markdown on server ([#7893](https://github.com/RocketChat/Rocket.Chat/pull/7893)) - Text area buttons and layout on mobile ([#7985](https://github.com/RocketChat/Rocket.Chat/pull/7985)) - Double scroll on 'keyboard shortcuts' menu in sidepanel ([#7927](https://github.com/RocketChat/Rocket.Chat/pull/7927) by [@aditya19496](https://github.com/aditya19496)) -- Broken embedded view layout ([#7944](https://github.com/RocketChat/Rocket.Chat/pull/7944)) +- Broken embedded view layout ([#7944](https://github.com/RocketChat/Rocket.Chat/pull/7944) by [@karlprieb](https://github.com/karlprieb)) - Textarea on firefox ([#7986](https://github.com/RocketChat/Rocket.Chat/pull/7986)) - Chat box no longer auto-focuses when typing ([#7984](https://github.com/RocketChat/Rocket.Chat/pull/7984)) - Add padding on messages to allow space to the action buttons ([#7971](https://github.com/RocketChat/Rocket.Chat/pull/7971)) - Small alignment fixes ([#7970](https://github.com/RocketChat/Rocket.Chat/pull/7970)) - Markdown being rendered in code tags ([#7965](https://github.com/RocketChat/Rocket.Chat/pull/7965)) - Fix the status on the members list ([#7963](https://github.com/RocketChat/Rocket.Chat/pull/7963)) -- status and active room colors on sidebar ([#7960](https://github.com/RocketChat/Rocket.Chat/pull/7960)) -- OTR buttons padding ([#7954](https://github.com/RocketChat/Rocket.Chat/pull/7954)) -- username ellipsis on firefox ([#7953](https://github.com/RocketChat/Rocket.Chat/pull/7953)) +- status and active room colors on sidebar ([#7960](https://github.com/RocketChat/Rocket.Chat/pull/7960) by [@karlprieb](https://github.com/karlprieb)) +- OTR buttons padding ([#7954](https://github.com/RocketChat/Rocket.Chat/pull/7954) by [@karlprieb](https://github.com/karlprieb)) +- username ellipsis on firefox ([#7953](https://github.com/RocketChat/Rocket.Chat/pull/7953) by [@karlprieb](https://github.com/karlprieb)) - Document README.md. Drupal repo out of date ([#7948](https://github.com/RocketChat/Rocket.Chat/pull/7948) by [@Lawri-van-Buel](https://github.com/Lawri-van-Buel)) - Fix placeholders in account profile ([#7945](https://github.com/RocketChat/Rocket.Chat/pull/7945) by [@josiasds](https://github.com/josiasds)) -- Broken emoji picker on firefox ([#7943](https://github.com/RocketChat/Rocket.Chat/pull/7943)) -- Create channel button on Firefox ([#7942](https://github.com/RocketChat/Rocket.Chat/pull/7942)) +- Broken emoji picker on firefox ([#7943](https://github.com/RocketChat/Rocket.Chat/pull/7943) by [@karlprieb](https://github.com/karlprieb)) +- Create channel button on Firefox ([#7942](https://github.com/RocketChat/Rocket.Chat/pull/7942) by [@karlprieb](https://github.com/karlprieb)) - Show leader on first load ([#7712](https://github.com/RocketChat/Rocket.Chat/pull/7712) by [@danischreiber](https://github.com/danischreiber)) -- Vertical menu on flex-tab ([#7988](https://github.com/RocketChat/Rocket.Chat/pull/7988)) +- Vertical menu on flex-tab ([#7988](https://github.com/RocketChat/Rocket.Chat/pull/7988) by [@karlprieb](https://github.com/karlprieb)) - Invisible leader bar on hover ([#8048](https://github.com/RocketChat/Rocket.Chat/pull/8048)) - Prevent autotranslate tokens race condition ([#8046](https://github.com/RocketChat/Rocket.Chat/pull/8046)) -- copy to clipboard and update clipboard.js library ([#8039](https://github.com/RocketChat/Rocket.Chat/pull/8039)) -- message-box autogrow ([#8019](https://github.com/RocketChat/Rocket.Chat/pull/8019)) -- search results height ([#8018](https://github.com/RocketChat/Rocket.Chat/pull/8018) by [@gdelavald](https://github.com/gdelavald)) -- room icon on header ([#8017](https://github.com/RocketChat/Rocket.Chat/pull/8017)) +- copy to clipboard and update clipboard.js library ([#8039](https://github.com/RocketChat/Rocket.Chat/pull/8039) by [@karlprieb](https://github.com/karlprieb)) +- message-box autogrow ([#8019](https://github.com/RocketChat/Rocket.Chat/pull/8019) by [@karlprieb](https://github.com/karlprieb)) +- search results height ([#8018](https://github.com/RocketChat/Rocket.Chat/pull/8018) by [@gdelavald](https://github.com/gdelavald) & [@karlprieb](https://github.com/karlprieb)) +- room icon on header ([#8017](https://github.com/RocketChat/Rocket.Chat/pull/8017) by [@karlprieb](https://github.com/karlprieb)) - Hide scrollbar on login page if not necessary ([#8014](https://github.com/RocketChat/Rocket.Chat/pull/8014)) - Error when translating message ([#8001](https://github.com/RocketChat/Rocket.Chat/pull/8001)) - Recent emojis not updated when adding via text ([#7998](https://github.com/RocketChat/Rocket.Chat/pull/7998)) - [PL] Polish translation ([#7989](https://github.com/RocketChat/Rocket.Chat/pull/7989) by [@Rzeszow](https://github.com/Rzeszow)) - Fix email on mention ([#7754](https://github.com/RocketChat/Rocket.Chat/pull/7754)) - RTL ([#8112](https://github.com/RocketChat/Rocket.Chat/pull/8112)) -- Dynamic popover ([#8101](https://github.com/RocketChat/Rocket.Chat/pull/8101)) +- Dynamic popover ([#8101](https://github.com/RocketChat/Rocket.Chat/pull/8101) by [@karlprieb](https://github.com/karlprieb)) - Settings description not showing ([#8122](https://github.com/RocketChat/Rocket.Chat/pull/8122)) - Fix setting user avatar on LDAP login ([#8099](https://github.com/RocketChat/Rocket.Chat/pull/8099)) - Not sending email to mentioned users with unchanged preference ([#8059](https://github.com/RocketChat/Rocket.Chat/pull/8059)) - Scroll on messagebox ([#8047](https://github.com/RocketChat/Rocket.Chat/pull/8047)) - Allow unknown file types if no allowed whitelist has been set (#7074) ([#8172](https://github.com/RocketChat/Rocket.Chat/pull/8172) by [@TriPhoenix](https://github.com/TriPhoenix)) - Issue #8166 where empty analytics setting breaks to load Piwik script ([#8167](https://github.com/RocketChat/Rocket.Chat/pull/8167) by [@ruKurz](https://github.com/ruKurz)) -- Sidebar and RTL alignments ([#8154](https://github.com/RocketChat/Rocket.Chat/pull/8154)) +- Sidebar and RTL alignments ([#8154](https://github.com/RocketChat/Rocket.Chat/pull/8154) by [@karlprieb](https://github.com/karlprieb)) - "*.members" rest api being useless and only returning usernames ([#8147](https://github.com/RocketChat/Rocket.Chat/pull/8147)) - Fix iframe login API response (issue #8145) ([#8146](https://github.com/RocketChat/Rocket.Chat/pull/8146) by [@astax-t](https://github.com/astax-t)) - Text area lost text when page reloads ([#8159](https://github.com/RocketChat/Rocket.Chat/pull/8159)) - Fix new room sound being played too much ([#8144](https://github.com/RocketChat/Rocket.Chat/pull/8144)) - Add admin audio preferences translations ([#8094](https://github.com/RocketChat/Rocket.Chat/pull/8094)) -- Leave and hide buttons was removed ([#8213](https://github.com/RocketChat/Rocket.Chat/pull/8213)) +- Leave and hide buttons was removed ([#8213](https://github.com/RocketChat/Rocket.Chat/pull/8213) by [@karlprieb](https://github.com/karlprieb)) - Incorrect URL for login terms when using prefix ([#8211](https://github.com/RocketChat/Rocket.Chat/pull/8211) by [@Darkneon](https://github.com/Darkneon)) - User avatar in DM list. ([#8210](https://github.com/RocketChat/Rocket.Chat/pull/8210)) - Scrollbar not using new style ([#8190](https://github.com/RocketChat/Rocket.Chat/pull/8190)) -- sidenav colors, hide and leave, create channel on safari ([#8257](https://github.com/RocketChat/Rocket.Chat/pull/8257)) -- make sidebar item animation fast ([#8262](https://github.com/RocketChat/Rocket.Chat/pull/8262)) -- RTL on reply ([#8261](https://github.com/RocketChat/Rocket.Chat/pull/8261)) -- clipboard and permalink on new popover ([#8259](https://github.com/RocketChat/Rocket.Chat/pull/8259)) -- sidenav mentions on hover ([#8252](https://github.com/RocketChat/Rocket.Chat/pull/8252)) +- sidenav colors, hide and leave, create channel on safari ([#8257](https://github.com/RocketChat/Rocket.Chat/pull/8257) by [@karlprieb](https://github.com/karlprieb)) +- make sidebar item animation fast ([#8262](https://github.com/RocketChat/Rocket.Chat/pull/8262) by [@karlprieb](https://github.com/karlprieb)) +- RTL on reply ([#8261](https://github.com/RocketChat/Rocket.Chat/pull/8261) by [@karlprieb](https://github.com/karlprieb)) +- clipboard and permalink on new popover ([#8259](https://github.com/RocketChat/Rocket.Chat/pull/8259) by [@karlprieb](https://github.com/karlprieb)) +- sidenav mentions on hover ([#8252](https://github.com/RocketChat/Rocket.Chat/pull/8252) by [@karlprieb](https://github.com/karlprieb)) - Api groups.files is always returning empty ([#8241](https://github.com/RocketChat/Rocket.Chat/pull/8241)) - Case insensitive SAML email check ([#8216](https://github.com/RocketChat/Rocket.Chat/pull/8216) by [@arminfelder](https://github.com/arminfelder)) - Execute meteor reset on TRAVIS_TAG builds ([#8310](https://github.com/RocketChat/Rocket.Chat/pull/8310)) -- Call buttons with wrong margin on RTL ([#8307](https://github.com/RocketChat/Rocket.Chat/pull/8307)) -- Emoji Picker hidden for reactions in RTL ([#8300](https://github.com/RocketChat/Rocket.Chat/pull/8300)) -- Amin menu not showing all items & File list breaking line ([#8299](https://github.com/RocketChat/Rocket.Chat/pull/8299)) +- Call buttons with wrong margin on RTL ([#8307](https://github.com/RocketChat/Rocket.Chat/pull/8307) by [@karlprieb](https://github.com/karlprieb)) +- Emoji Picker hidden for reactions in RTL ([#8300](https://github.com/RocketChat/Rocket.Chat/pull/8300) by [@karlprieb](https://github.com/karlprieb)) +- Amin menu not showing all items & File list breaking line ([#8299](https://github.com/RocketChat/Rocket.Chat/pull/8299) by [@karlprieb](https://github.com/karlprieb)) - TypeError: Cannot read property 't' of undefined ([#8298](https://github.com/RocketChat/Rocket.Chat/pull/8298)) - Wrong file name when upload to AWS S3 ([#8296](https://github.com/RocketChat/Rocket.Chat/pull/8296)) - Check attachments is defined before accessing first element ([#8295](https://github.com/RocketChat/Rocket.Chat/pull/8295) by [@Darkneon](https://github.com/Darkneon)) @@ -2291,12 +4483,12 @@ - "Channel Setting" buttons alignment in RTL ([#8266](https://github.com/RocketChat/Rocket.Chat/pull/8266) by [@cyclops24](https://github.com/cyclops24)) - Removing pipe and commas from custom emojis (#8168) ([#8237](https://github.com/RocketChat/Rocket.Chat/pull/8237) by [@matheusml](https://github.com/matheusml)) - After deleting the room, cache is not synchronizing ([#8314](https://github.com/RocketChat/Rocket.Chat/pull/8314) by [@szluohua](https://github.com/szluohua)) -- Remove sidebar header on admin embedded version ([#8334](https://github.com/RocketChat/Rocket.Chat/pull/8334)) +- Remove sidebar header on admin embedded version ([#8334](https://github.com/RocketChat/Rocket.Chat/pull/8334) by [@karlprieb](https://github.com/karlprieb)) - Email Subjects not being sent ([#8317](https://github.com/RocketChat/Rocket.Chat/pull/8317)) -- Put delete action on another popover group ([#8315](https://github.com/RocketChat/Rocket.Chat/pull/8315)) +- Put delete action on another popover group ([#8315](https://github.com/RocketChat/Rocket.Chat/pull/8315) by [@karlprieb](https://github.com/karlprieb)) - Mention unread indicator was removed ([#8316](https://github.com/RocketChat/Rocket.Chat/pull/8316)) - Various LDAP issues & Missing pagination ([#8372](https://github.com/RocketChat/Rocket.Chat/pull/8372)) -- remove accountBox from admin menu ([#8358](https://github.com/RocketChat/Rocket.Chat/pull/8358)) +- remove accountBox from admin menu ([#8358](https://github.com/RocketChat/Rocket.Chat/pull/8358) by [@karlprieb](https://github.com/karlprieb)) - Missing i18n translations ([#8357](https://github.com/RocketChat/Rocket.Chat/pull/8357)) - Sidebar item menu position in RTL ([#8397](https://github.com/RocketChat/Rocket.Chat/pull/8397) by [@cyclops24](https://github.com/cyclops24)) - disabled katex tooltip on messageBox ([#8386](https://github.com/RocketChat/Rocket.Chat/pull/8386)) @@ -2330,30 +4522,30 @@ - Fix typo in generated URI ([#7661](https://github.com/RocketChat/Rocket.Chat/pull/7661) by [@Rohlik](https://github.com/Rohlik)) - Bump version to 0.59.0-develop ([#7625](https://github.com/RocketChat/Rocket.Chat/pull/7625)) - implemented new page-loader animated icon ([#2](https://github.com/RocketChat/Rocket.Chat/pull/2)) -- Hide flex-tab close button ([#7894](https://github.com/RocketChat/Rocket.Chat/pull/7894)) +- Hide flex-tab close button ([#7894](https://github.com/RocketChat/Rocket.Chat/pull/7894) by [@karlprieb](https://github.com/karlprieb)) - Update BlackDuck URL ([#7941](https://github.com/RocketChat/Rocket.Chat/pull/7941)) -- [DOCS] Add native mobile app links into README and update button images ([#7909](https://github.com/RocketChat/Rocket.Chat/pull/7909)) +- [DOCS] Add native mobile app links into README and update button images ([#7909](https://github.com/RocketChat/Rocket.Chat/pull/7909) by [@rafaelks](https://github.com/rafaelks)) - Remove unnecessary returns in cors common ([#8054](https://github.com/RocketChat/Rocket.Chat/pull/8054) by [@Kiran-Rao](https://github.com/Kiran-Rao)) - npm deps update ([#8197](https://github.com/RocketChat/Rocket.Chat/pull/8197)) -- Fix more rtl issues ([#8194](https://github.com/RocketChat/Rocket.Chat/pull/8194)) +- Fix more rtl issues ([#8194](https://github.com/RocketChat/Rocket.Chat/pull/8194) by [@karlprieb](https://github.com/karlprieb)) - readme-file: fix broken link ([#8253](https://github.com/RocketChat/Rocket.Chat/pull/8253) by [@vcapretz](https://github.com/vcapretz)) - Disable perfect scrollbar ([#8244](https://github.com/RocketChat/Rocket.Chat/pull/8244)) -- Fix `leave and hide` click, color and position ([#8243](https://github.com/RocketChat/Rocket.Chat/pull/8243)) +- Fix `leave and hide` click, color and position ([#8243](https://github.com/RocketChat/Rocket.Chat/pull/8243) by [@karlprieb](https://github.com/karlprieb)) - Deps update ([#8273](https://github.com/RocketChat/Rocket.Chat/pull/8273)) - Update meteor to 1.5.2.2-rc.0 ([#8355](https://github.com/RocketChat/Rocket.Chat/pull/8355)) -- [FIX-RC] Mobile file upload not working ([#8331](https://github.com/RocketChat/Rocket.Chat/pull/8331)) +- [FIX-RC] Mobile file upload not working ([#8331](https://github.com/RocketChat/Rocket.Chat/pull/8331) by [@karlprieb](https://github.com/karlprieb)) - LingoHub based on develop ([#8375](https://github.com/RocketChat/Rocket.Chat/pull/8375)) - Update Meteor to 1.5.2.2 ([#8364](https://github.com/RocketChat/Rocket.Chat/pull/8364)) - Sync translations from LingoHub ([#8363](https://github.com/RocketChat/Rocket.Chat/pull/8363)) - Remove field `lastActivity` from subscription data ([#8345](https://github.com/RocketChat/Rocket.Chat/pull/8345)) -- Fix: Account menu position on RTL ([#8416](https://github.com/RocketChat/Rocket.Chat/pull/8416)) +- Fix: Account menu position on RTL ([#8416](https://github.com/RocketChat/Rocket.Chat/pull/8416) by [@karlprieb](https://github.com/karlprieb)) - Fix: Missing LDAP option to show internal logs ([#8417](https://github.com/RocketChat/Rocket.Chat/pull/8417)) - Fix: Missing LDAP reconnect setting ([#8414](https://github.com/RocketChat/Rocket.Chat/pull/8414)) - Add i18n Title to snippet messages ([#8394](https://github.com/RocketChat/Rocket.Chat/pull/8394)) - Fix: Missing settings to configure LDAP size and page limits ([#8398](https://github.com/RocketChat/Rocket.Chat/pull/8398)) - Improve markdown parser code ([#8451](https://github.com/RocketChat/Rocket.Chat/pull/8451)) - Change artifact path ([#8515](https://github.com/RocketChat/Rocket.Chat/pull/8515)) -- Color variables migration ([#8463](https://github.com/RocketChat/Rocket.Chat/pull/8463)) +- Color variables migration ([#8463](https://github.com/RocketChat/Rocket.Chat/pull/8463) by [@karlprieb](https://github.com/karlprieb)) - Fix: Change password not working in new UI ([#8516](https://github.com/RocketChat/Rocket.Chat/pull/8516)) - Enable AutoLinker back ([#8490](https://github.com/RocketChat/Rocket.Chat/pull/8490)) - Fix artifact path ([#8518](https://github.com/RocketChat/Rocket.Chat/pull/8518)) @@ -2385,12 +4577,14 @@ - [@gdelavald](https://github.com/gdelavald) - [@goiaba](https://github.com/goiaba) - [@josiasds](https://github.com/josiasds) +- [@karlprieb](https://github.com/karlprieb) - [@luizbills](https://github.com/luizbills) - [@maarten-v](https://github.com/maarten-v) - [@matheusml](https://github.com/matheusml) - [@mboudet](https://github.com/mboudet) - [@nishimaki10](https://github.com/nishimaki10) - [@pkgodara](https://github.com/pkgodara) +- [@rafaelks](https://github.com/rafaelks) - [@rdebeasi](https://github.com/rdebeasi) - [@ruKurz](https://github.com/ruKurz) - [@snoozan](https://github.com/snoozan) @@ -2406,9 +4600,7 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@marceloschmidt](https://github.com/marceloschmidt) -- [@rafaelks](https://github.com/rafaelks) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -2499,7 +4691,7 @@ - Add toolbar buttons for iframe API ([#7525](https://github.com/RocketChat/Rocket.Chat/pull/7525)) - Add close button to flex tabs ([#7529](https://github.com/RocketChat/Rocket.Chat/pull/7529)) - Update meteor to 1.5.1 ([#7496](https://github.com/RocketChat/Rocket.Chat/pull/7496)) -- flex-tab now is side by side with message list ([#7448](https://github.com/RocketChat/Rocket.Chat/pull/7448)) +- flex-tab now is side by side with message list ([#7448](https://github.com/RocketChat/Rocket.Chat/pull/7448) by [@karlprieb](https://github.com/karlprieb)) - Option to select unread count behavior ([#7477](https://github.com/RocketChat/Rocket.Chat/pull/7477)) - Add healthchecks in OpenShift templates ([#7184](https://github.com/RocketChat/Rocket.Chat/pull/7184) by [@jfchevrette](https://github.com/jfchevrette)) - Add unread options for direct messages ([#7658](https://github.com/RocketChat/Rocket.Chat/pull/7658)) @@ -2524,7 +4716,7 @@ - Fix Word Placement Anywhere on WebHooks ([#7392](https://github.com/RocketChat/Rocket.Chat/pull/7392)) - Prevent new room status from playing when user status changes ([#7487](https://github.com/RocketChat/Rocket.Chat/pull/7487)) - url click events in the cordova app open in external browser or not at all ([#7205](https://github.com/RocketChat/Rocket.Chat/pull/7205) by [@flaviogrossi](https://github.com/flaviogrossi)) -- sweetalert alignment on mobile ([#7404](https://github.com/RocketChat/Rocket.Chat/pull/7404)) +- sweetalert alignment on mobile ([#7404](https://github.com/RocketChat/Rocket.Chat/pull/7404) by [@karlprieb](https://github.com/karlprieb)) - Sweet-Alert modal popup position on mobile devices ([#7376](https://github.com/RocketChat/Rocket.Chat/pull/7376) by [@Oliver84](https://github.com/Oliver84)) - Update node-engine in Snap to latest v4 LTS relase: 4.8.3 ([#7355](https://github.com/RocketChat/Rocket.Chat/pull/7355) by [@al3x](https://github.com/al3x)) - Remove warning about 2FA support being unavailable in mobile apps ([#7354](https://github.com/RocketChat/Rocket.Chat/pull/7354) by [@al3x](https://github.com/al3x)) @@ -2537,13 +4729,13 @@ - Fix room load on first hit ([#7687](https://github.com/RocketChat/Rocket.Chat/pull/7687)) - Markdown noopener/noreferrer: use correct HTML attribute ([#7644](https://github.com/RocketChat/Rocket.Chat/pull/7644) by [@jangmarker](https://github.com/jangmarker)) - Wrong email subject when "All Messages" setting enabled ([#7639](https://github.com/RocketChat/Rocket.Chat/pull/7639)) -- Csv importer: work with more problematic data ([#7456](https://github.com/RocketChat/Rocket.Chat/pull/7456) by [@reist](https://github.com/reist)) +- Csv importer: work with more problematic data ([#7456](https://github.com/RocketChat/Rocket.Chat/pull/7456)) - make flex-tab visible again when reduced width ([#7738](https://github.com/RocketChat/Rocket.Chat/pull/7738))
🔍 Minor changes -- Release 0.58.0 ([#7752](https://github.com/RocketChat/Rocket.Chat/pull/7752) by [@flaviogrossi](https://github.com/flaviogrossi) & [@jangmarker](https://github.com/jangmarker) & [@ryoshimizu](https://github.com/ryoshimizu)) +- Release 0.58.0 ([#7752](https://github.com/RocketChat/Rocket.Chat/pull/7752) by [@flaviogrossi](https://github.com/flaviogrossi) & [@jangmarker](https://github.com/jangmarker) & [@karlprieb](https://github.com/karlprieb) & [@ryoshimizu](https://github.com/ryoshimizu)) - Sync Master with 0.57.3 ([#7690](https://github.com/RocketChat/Rocket.Chat/pull/7690)) - Add missing parts of `one click to direct message` ([#7608](https://github.com/RocketChat/Rocket.Chat/pull/7608)) - LingoHub based on develop ([#7613](https://github.com/RocketChat/Rocket.Chat/pull/7613)) @@ -2578,8 +4770,8 @@ - [@gdelavald](https://github.com/gdelavald) - [@jangmarker](https://github.com/jangmarker) - [@jfchevrette](https://github.com/jfchevrette) +- [@karlprieb](https://github.com/karlprieb) - [@lindoelio](https://github.com/lindoelio) -- [@reist](https://github.com/reist) - [@ruKurz](https://github.com/ruKurz) - [@ryoshimizu](https://github.com/ryoshimizu) - [@satyapramodh](https://github.com/satyapramodh) @@ -2597,8 +4789,8 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@pierreozoux](https://github.com/pierreozoux) +- [@reist](https://github.com/reist) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -2716,7 +4908,7 @@ - Migration to add tags to email header and footer ([#7080](https://github.com/RocketChat/Rocket.Chat/pull/7080)) - postcss parser and cssnext implementation ([#6982](https://github.com/RocketChat/Rocket.Chat/pull/6982)) - Start running unit tests ([#6605](https://github.com/RocketChat/Rocket.Chat/pull/6605)) -- Make channel/group delete call answer to roomName ([#6857](https://github.com/RocketChat/Rocket.Chat/pull/6857) by [@reist](https://github.com/reist)) +- Make channel/group delete call answer to roomName ([#6857](https://github.com/RocketChat/Rocket.Chat/pull/6857)) - Feature/delete any message permission ([#6919](https://github.com/RocketChat/Rocket.Chat/pull/6919) by [@phutchins](https://github.com/phutchins)) - Force use of MongoDB for spotlight queries ([#7311](https://github.com/RocketChat/Rocket.Chat/pull/7311)) @@ -2817,7 +5009,6 @@ - [@matthewshirley](https://github.com/matthewshirley) - [@phutchins](https://github.com/phutchins) - [@pmb0](https://github.com/pmb0) -- [@reist](https://github.com/reist) - [@sathieu](https://github.com/sathieu) - [@thinkeridea](https://github.com/thinkeridea) @@ -2831,6 +5022,7 @@ - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) - [@marceloschmidt](https://github.com/marceloschmidt) +- [@reist](https://github.com/reist) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -2844,11 +5036,11 @@ ### 🎉 New features - Add a pointer cursor to message images ([#6881](https://github.com/RocketChat/Rocket.Chat/pull/6881)) -- Make channels.info accept roomName, just like groups.info ([#6827](https://github.com/RocketChat/Rocket.Chat/pull/6827) by [@reist](https://github.com/reist)) +- Make channels.info accept roomName, just like groups.info ([#6827](https://github.com/RocketChat/Rocket.Chat/pull/6827)) - Option to allow to signup as anonymous ([#6797](https://github.com/RocketChat/Rocket.Chat/pull/6797)) - create a method 'create token' ([#6807](https://github.com/RocketChat/Rocket.Chat/pull/6807)) - Add option on Channel Settings: Hide Notifications and Hide Unread Room Status (#2707, #2143) ([#5373](https://github.com/RocketChat/Rocket.Chat/pull/5373)) -- Remove lesshat ([#6722](https://github.com/RocketChat/Rocket.Chat/pull/6722)) +- Remove lesshat ([#6722](https://github.com/RocketChat/Rocket.Chat/pull/6722) by [@karlprieb](https://github.com/karlprieb)) - Use tokenSentVia parameter for clientid/secret to token endpoint ([#6692](https://github.com/RocketChat/Rocket.Chat/pull/6692) by [@intelradoux](https://github.com/intelradoux)) - Add a setting to not run outgoing integrations on message edits ([#6615](https://github.com/RocketChat/Rocket.Chat/pull/6615)) - Improve CI/Docker build/release ([#6938](https://github.com/RocketChat/Rocket.Chat/pull/6938)) @@ -2868,14 +5060,14 @@ - Hides nav buttons when selecting own profile ([#6760](https://github.com/RocketChat/Rocket.Chat/pull/6760) by [@gdelavald](https://github.com/gdelavald)) - Search full name on client side ([#6767](https://github.com/RocketChat/Rocket.Chat/pull/6767)) - Sort by real name if use real name setting is enabled ([#6758](https://github.com/RocketChat/Rocket.Chat/pull/6758)) -- CSV importer: require that there is some data in the zip, not ALL data ([#6768](https://github.com/RocketChat/Rocket.Chat/pull/6768) by [@reist](https://github.com/reist)) +- CSV importer: require that there is some data in the zip, not ALL data ([#6768](https://github.com/RocketChat/Rocket.Chat/pull/6768)) - Archiving Direct Messages ([#6737](https://github.com/RocketChat/Rocket.Chat/pull/6737)) - Fix Caddy by forcing go 1.7 as needed by one of caddy's dependencies ([#6721](https://github.com/RocketChat/Rocket.Chat/pull/6721)) - Users status on main menu always offline ([#6896](https://github.com/RocketChat/Rocket.Chat/pull/6896)) - Not showing unread count on electron app’s icon ([#6923](https://github.com/RocketChat/Rocket.Chat/pull/6923)) - Compile CSS color variables ([#6939](https://github.com/RocketChat/Rocket.Chat/pull/6939)) - Remove spaces from env PORT and INSTANCE_IP ([#6955](https://github.com/RocketChat/Rocket.Chat/pull/6955)) -- make channels.create API check for create-c ([#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968) by [@reist](https://github.com/reist)) +- make channels.create API check for create-c ([#6968](https://github.com/RocketChat/Rocket.Chat/pull/6968))
🔍 Minor changes @@ -2909,7 +5101,7 @@ - [@gdelavald](https://github.com/gdelavald) - [@glehmann](https://github.com/glehmann) - [@intelradoux](https://github.com/intelradoux) -- [@reist](https://github.com/reist) +- [@karlprieb](https://github.com/karlprieb) - [@robertdown](https://github.com/robertdown) - [@sscholl](https://github.com/sscholl) - [@vlogic](https://github.com/vlogic) @@ -2922,8 +5114,8 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@marceloschmidt](https://github.com/marceloschmidt) +- [@reist](https://github.com/reist) - [@rodrigok](https://github.com/rodrigok) - [@sampaiodiego](https://github.com/sampaiodiego) @@ -2959,7 +5151,7 @@ ### 🎉 New features - Add shield.svg api route to generate custom shields/badges ([#6565](https://github.com/RocketChat/Rocket.Chat/pull/6565)) -- resolve merge share function ([#6577](https://github.com/RocketChat/Rocket.Chat/pull/6577) by [@tgxn](https://github.com/tgxn)) +- resolve merge share function ([#6577](https://github.com/RocketChat/Rocket.Chat/pull/6577) by [@karlprieb](https://github.com/karlprieb) & [@tgxn](https://github.com/tgxn)) - Two Factor Auth ([#6476](https://github.com/RocketChat/Rocket.Chat/pull/6476)) - Permission `join-without-join-code` assigned to admins and bots by default ([#6430](https://github.com/RocketChat/Rocket.Chat/pull/6430)) - Integrations, both incoming and outgoing, now have access to the models. Example: `Users.findOneById(id)` ([#6420](https://github.com/RocketChat/Rocket.Chat/pull/6420)) @@ -2974,7 +5166,7 @@ - Removed Deprecated Package rocketchat:sharedsecret` - emoji picker exception ([#6709](https://github.com/RocketChat/Rocket.Chat/pull/6709) by [@gdelavald](https://github.com/gdelavald)) - Large files crashed browser when trying to show preview ([#6598](https://github.com/RocketChat/Rocket.Chat/pull/6598)) -- messageBox: put "joinCodeRequired" back ([#6600](https://github.com/RocketChat/Rocket.Chat/pull/6600)) +- messageBox: put "joinCodeRequired" back ([#6600](https://github.com/RocketChat/Rocket.Chat/pull/6600) by [@karlprieb](https://github.com/karlprieb)) - Do not add default roles for users without services field ([#6594](https://github.com/RocketChat/Rocket.Chat/pull/6594)) - Accounts from LinkedIn OAuth without name ([#6590](https://github.com/RocketChat/Rocket.Chat/pull/6590)) - Usage of subtagged languages ([#6575](https://github.com/RocketChat/Rocket.Chat/pull/6575)) @@ -3057,7 +5249,7 @@ - Flex-Tab CoffeeScript to JavaScript II ([#6277](https://github.com/RocketChat/Rocket.Chat/pull/6277)) - Side-nav CoffeeScript to JavaScript II ([#6266](https://github.com/RocketChat/Rocket.Chat/pull/6266)) - Allow Livechat visitors to switch the department ([#6035](https://github.com/RocketChat/Rocket.Chat/pull/6035) by [@drallgood](https://github.com/drallgood)) -- fix livechat widget on small screens ([#6122](https://github.com/RocketChat/Rocket.Chat/pull/6122)) +- fix livechat widget on small screens ([#6122](https://github.com/RocketChat/Rocket.Chat/pull/6122) by [@karlprieb](https://github.com/karlprieb)) - Allow livechat managers to transfer chats ([#6180](https://github.com/RocketChat/Rocket.Chat/pull/6180) by [@drallgood](https://github.com/drallgood)) - focus first textbox element ([#6257](https://github.com/RocketChat/Rocket.Chat/pull/6257) by [@a5his](https://github.com/a5his)) - Join command ([#6268](https://github.com/RocketChat/Rocket.Chat/pull/6268)) @@ -3098,6 +5290,7 @@ - [@drallgood](https://github.com/drallgood) - [@fengt](https://github.com/fengt) - [@gdelavald](https://github.com/gdelavald) +- [@karlprieb](https://github.com/karlprieb) - [@nathanmarcos](https://github.com/nathanmarcos) - [@qge](https://github.com/qge) - [@sezinkarli](https://github.com/sezinkarli) @@ -3112,7 +5305,6 @@ - [@geekgonecrazy](https://github.com/geekgonecrazy) - [@ggazzo](https://github.com/ggazzo) - [@graywolf336](https://github.com/graywolf336) -- [@karlprieb](https://github.com/karlprieb) - [@marceloschmidt](https://github.com/marceloschmidt) - [@mrsimpson](https://github.com/mrsimpson) - [@rodrigok](https://github.com/rodrigok) diff --git a/LIMITATION_OF_RESPONSIBILITY.md b/LIMITATION_OF_RESPONSIBILITY.md new file mode 100644 index 000000000000..531868d2c0d3 --- /dev/null +++ b/LIMITATION_OF_RESPONSIBILITY.md @@ -0,0 +1,23 @@ +## WARNING + +Rocket.Chat is open source software. Anyone in the world can download and run a Rocket.Chat server at any time. + +As a user of Rocket.Chat, someone with a Rocket.Chat account, you need to be aware that you may be using a Rocket.Chat server that is operated by arbitrary businesses, groups or individuals with no relationship to Rocket.Chat Technologies Corp. + +In particular: + +- Rocket.Chat Technologies Corp. do not have access to these servers. +- Rocket.Chat Technologies Corp. do not and cannot control or regulate how these servers are operated. +- Rocket.Chat Technologies Corp. cannot access, determine or regulate any contents or information flow on these servers. + +## PUBLIC SERVER + +For total transparency, Rocket.Chat Technologies Corp. owns and operates only ONE publicly available Rocket.Chat server in the world. The server that Rocket.Chat Technologies Corp. operates can only be accessed at: + +https://open.rocket.chat + +Any other Rocket.Chat server you access is not operated by Rocket.Chat Technologies Corp. and is subjected to the usage warning above. + +## ROCKET.CHAT CLOUD + +Rocket.Chat Technologies Corp. provides a cloud service for hosting Rocket.Chat instances. The data, messages and files on those instances are subject to our [Terms of Use](https://rocket.chat/terms). If you have evidence of misuse or a breach of our terms, contact us at [contact@rocket.chat](mailto:contact@rocket.chat) and include a description of the breach as well as the instance's URL. diff --git a/README.md b/README.md index babf76c7a512..db33dd319d6c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -![Rocket.Chat logo](https://upload.wikimedia.org/wikipedia/commons/1/12/Rocket.Chat_Logo.svg) +![](https://user-images.githubusercontent.com/551004/43643393-884b00a4-9701-11e8-94d8-14c46d1f3660.png) -# The Ultimate Open Source WebChat Platform +

+ The Ultimate Open Source WebChat Platform +

[![Rocket.Chat](https://open.rocket.chat/images/join-chat.svg)](https://open.rocket.chat/) [![Build Status](https://img.shields.io/travis/RocketChat/Rocket.Chat/master.svg)](https://travis-ci.org/RocketChat/Rocket.Chat) @@ -11,14 +13,15 @@ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/RocketChat/Rocket.Chat/raw/master/LICENSE) +* [**NEW!** Help Wanted](#help-wanted) * [Community](#community) * [Mobile Apps](#mobile-apps) * [Desktop Apps](#desktop-apps) * [Deployment](#deployment) * [Snaps](#instant-server-installation-with-snaps) + * [DigitalOcean](#digitalocean-droplet) * [RocketChatLauncher](#rocketchatlauncher) * [Layershift](#layershift) - * [Sandstorm.io](#sandstormio) * [Yunohost.org](#yunohostorg) * [DPlatform](#dplatform) * [IndieHosters](#indiehosters) @@ -36,6 +39,7 @@ * [Hyper.sh](#hypersh) * [WeDeploy](#wedeploy) * [D2C.io](#d2cio) + * [Syncloud.org](#syncloudorg) * [About Rocket.Chat](#about-rocketchat) * [In the News](#in-the-news) * [Features](#features) @@ -55,6 +59,21 @@ * [Credits](#credits) * [Donate](#donate) +# Help Wanted + +At Rocket.Chat, our community drives *everything* we do. The Rocket.Chat team is expanding, and we know no better place to find qualified new team members than *right here* - in our Github community. + +If you are passionate about our project, want to work with a world-leading open source team, and enjoy working remotely at a location of your choice, then we want to talk to you! + +Explore current openings below: + +- [Lead Security Researcher and Developer](https://rocketchat.recruitee.com/o/lead-security-researcher-and-developer) + +- [Sales Engineer](https://rocketchat.recruitee.com/o/sales-engineer) + +- [Business Developer/Sales/Channel](https://rocketchat.recruitee.com/o/business-developer-sales-channel) + +- [Front-End Developer](https://rocketchat.recruitee.com/o/frontend-developer) # Community Join thousands of members worldwide 24/7 in our [community server](https://open.rocket.chat). @@ -63,7 +82,7 @@ Join thousands of members worldwide 24/7 in our [community server](https://open. [![Rocket.Chat](https://open.rocket.chat/api/v1/shield.svg?type=channel&name=Rocket.Chat&channel=dev)](https://open.rocket.chat/channel/dev) for developers needing help from the community to developing new features. -You can also join the conversation at [Twitter](https://twitter.com/RocketChat), [Facebook](https://www.facebook.com/RocketChatApp) or [Google Plus](https://plus.google.com/+RocketChatApp). +You can also join the conversation at [Twitter](https://twitter.com/RocketChat) and [Facebook](https://www.facebook.com/RocketChatApp). # Desktop Apps Download the Native Cross-Platform Desktop Application at [Rocket.Chat.Electron](https://github.com/RocketChat/Rocket.Chat.Electron/releases) @@ -71,17 +90,7 @@ Download the Native Cross-Platform Desktop Application at [Rocket.Chat.Electron] # Mobile Apps -## Native Mobile Apps -*Note: currently the native apps doesn't support all the features that web does. If you're looking for it, you should download the Cordova apps.* - -[![Rocket.Chat on Apple App Store](https://user-images.githubusercontent.com/551004/29770691-a2082ff4-8bc6-11e7-89a6-964cd405ea8e.png)](https://itunes.apple.com/us/app/rocket-chat/id1148741252?mt=8) [![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) - -## Hybrid Mobile Apps (Cordova) - -[![Rocket.Chat on Apple App Store](https://user-images.githubusercontent.com/551004/29770691-a2082ff4-8bc6-11e7-89a6-964cd405ea8e.png)](https://itunes.apple.com/us/app/rocket.chat/id1028869439?mt=8) [![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=com.konecty.rocket.chat) - -*Now compatible with all Android devices as old as version 4.0.x - [download here](https://rocket.chat/docs/developer-guides/mobile-apps/), even on BlackBerry Passport!* - +[![Rocket.Chat on Apple App Store](https://user-images.githubusercontent.com/551004/29770691-a2082ff4-8bc6-11e7-89a6-964cd405ea8e.png)](https://itunes.apple.com/us/app/rocket-chat/id1148741252?mt=8) [![Rocket.Chat on Google Play](https://user-images.githubusercontent.com/551004/29770692-a20975c6-8bc6-11e7-8ab0-1cde275496e0.png)](https://play.google.com/store/apps/details?id=chat.rocket.android) [![](https://user-images.githubusercontent.com/551004/48210349-50649480-e35e-11e8-97d9-74a4331faf3a.png)](https://f-droid.org/en/packages/chat.rocket.android/) # Deployment @@ -95,12 +104,18 @@ sudo snap install rocketchat-server [![Rocket.Chat Snap is recommended for Linux deployments](https://github.com/Sing-Li/bbug/raw/master/images/ubuntulogo.png)](https://uappexplorer.com/snap/ubuntu/rocketchat-server) -Installing snaps is very quick. By running that command you have your full Rocket.Chat server up and running. Snaps are secure. They are isolated with all of their dependencies. Snaps also auto update when we release new versions. +Installing snaps is very quick. By running that command you have your full Rocket.Chat server up and running. Snaps are secure. They are isolated with all of their dependencies. Snaps also auto-update when we release new versions. Our snap features a built-in reverse proxy that can request and maintain free Let's Encrypt SSL certificates. You can go from zero to a public-facing SSL-secured Rocket.Chat server in less than 5 minutes. Find out more information about our snaps [here](https://rocket.chat/docs/installation/manual-installation/ubuntu/snaps/). +## DigitalOcean droplet + +Deploy to a DigitalOcean droplet with our one-click install listing from the DigitalOcean Marketplace. + +[![do-btn-blue](https://user-images.githubusercontent.com/51996/58146107-50512580-7c1a-11e9-8ec9-e032ba387c2a.png)](https://marketplace.digitalocean.com/apps/rocket-chat?action=deploy&refcode=1940fe28bd31) + ## RocketChatLauncher Focus on your team/community and not on servers or code - the Launcher provides RocketChat-as-a-Service on a monthly subscription model. @@ -115,11 +130,6 @@ Instantly deploy your Rocket.Chat server for free on next generation auto-scalin Painless SSL. Automatically scale your server cluster based on usage demand. -## Sandstorm.io -Host your own Rocket.Chat server in four seconds flat. - -[![Rocket.Chat on Sandstorm.io](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/sandstorm.jpg)](https://apps.sandstorm.io/app/vfnwptfn02ty21w715snyyczw0nqxkv3jvawcah10c6z7hj1hnu0) - ## Yunohost.org Host your own Rocket.Chat server in a few seconds. @@ -127,7 +137,7 @@ Host your own Rocket.Chat server in a few seconds. ## DPlatform -Easiest way to install a ready-to-run Rocket.Chat server on a Linux machine, VM, or VPS. +The easiest way to install a ready-to-run Rocket.Chat server on a Linux machine, VM, or VPS. [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/images/logo.png)](https://dfabric.github.io/DPlatform-ShellCore) @@ -157,7 +167,7 @@ Host your own Rocket.Chat server for **FREE** with [One-Click Deploy](https://he [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/RocketChat/Rocket.Chat/tree/master) ## Helm Kubernetes -Deploy on Kubernetes using the official [helm chart](https://github.com/kubernetes/charts/pull/752). +Deploy on Kubernetes using the official [helm chart](https://github.com/helm/charts/tree/master/stable/rocketchat). ## Scalingo Deploy your own Rocket.Chat server instantly on [Scalingo](https://scalingo.com). @@ -197,7 +207,7 @@ Automated production-grade deployment in minutes, for RHEL / CentOS 7 or Ubuntu [![Ansible deployment](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/ansible.png)](https://rocket.chat/docs/installation/automation-tools/ansible/) ## Raspberry Pi 2 -Run Rocket.Chat on this world famous $30 quad core server. +Run Rocket.Chat on this world famous $30 quad-core server. [![Raspberry Pi 2](https://raw.githubusercontent.com/Sing-Li/bbug/master/images/pitiny.png)](https://github.com/RocketChat/Rocket.Chat.RaspberryPi) @@ -223,6 +233,11 @@ Deploy Rocket.Chat stack to your server with [D2C](https://d2c.io/). Scale with [![Deploy](https://github.com/mastappl/images/blob/master/deployTo.png)](https://panel.d2c.io/?import=https://github.com/d2cio/rocketchat-stack/archive/master.zip/) +## Syncloud.org +Run Rocket.Chat on your easy to use personal device. + +[![Deploy](https://syncloud.org/images/logo_min.svg)](https://syncloud.org) + # About Rocket.Chat Rocket.Chat is a Web Chat Server, developed in JavaScript, using the [Meteor](https://www.meteor.com/install) fullstack framework. @@ -294,7 +309,7 @@ It is a great solution for communities and companies wanting to privately host t - (Beta) Jitsi integration - Audio calls - Multi-users Audio Conference -- Screensharing +- Screen sharing - Drupal 7.x and 8.x Plug-in (both stable and development flavours) ([download](https://www.drupal.org/project/rocket_chat) and [source code](https://git.drupal.org/project/rocket_chat.git) ) - XMPP bridge ([try it](https://open.rocket.chat/channel/general)) - REST APIs @@ -306,7 +321,6 @@ It is a great solution for communities and companies wanting to privately host t - Native Cross-Platform Desktop Application [Windows, macOS, or Linux](https://rocket.chat/) - Mobile app for iPhone, iPad, and iPod touch [Download on App Store](https://geo.itunes.apple.com/us/app/rocket-chat/id1148741252?mt=8) - Mobile app for Android phone, tablet, and TV stick [Available now on Google Play](https://play.google.com/store/apps/details?id=chat.rocket.android) -- Sandstorm.io instant Rocket.Chat server [Now on Sandstorm App Store](https://apps.sandstorm.io/app/vfnwptfn02ty21w715snyyczw0nqxkv3jvawcah10c6z7hj1hnu0) - Available on [Cloudron Store](https://cloudron.io/appstore.html#chat.rocket.cloudronapp) ## Roadmap @@ -315,9 +329,6 @@ It is a great solution for communities and companies wanting to privately host t - XMPP Support via [Webhook bridge](https://github.com/saqura/xmppwb) [Issue #404](https://github.com/RocketChat/Rocket.Chat/issues/404) - Federation via [matrix.org](https://www.matrix.org/), see [hubot-freddie](https://www.npmjs.com/package/hubot-freddie) and [Federation project](https://github.com/RocketChat/Rocket.Chat.Federation) : [Issue #520](https://github.com/RocketChat/Rocket.Chat/issues/520), [Issue #601](https://github.com/RocketChat/Rocket.Chat/issues/601) - Support for PostgreSQL: [Issue #533](https://github.com/RocketChat/Rocket.Chat/issues/533), [Issue #822](https://github.com/RocketChat/Rocket.Chat/pull/822) -- Native iOS Application [Issue #270](https://github.com/RocketChat/Rocket.Chat/issues/270), [Rocket.Chat.iOS - HELP WANTED](https://github.com/RocketChat/Rocket.Chat.iOS) -- Native Android Application [Issue #271 - HELP WANTED](https://github.com/RocketChat/Rocket.Chat/issues/271) -- Off the Record Messaging [Issue #36](https://github.com/RocketChat/Rocket.Chat/issues/36), [Issue #268](https://github.com/RocketChat/Rocket.Chat/issues/268) - WordPress Plug-in [Issue # 1920](https://github.com/RocketChat/Rocket.Chat/issues/1920) - Integration with PSTN (Public Switched Telephone Networks) - API-enabled methods: [Issue #202](https://github.com/RocketChat/Rocket.Chat/issues/202), [Issue #454](https://github.com/RocketChat/Rocket.Chat/issues/454), [Issue #455](https://github.com/RocketChat/Rocket.Chat/issues/455), [Issue #759](https://github.com/RocketChat/Rocket.Chat/issues/759) @@ -351,7 +362,7 @@ Read about [how it all started](https://blog.blackducksoftware.com/rocket-chat-e ## Feature Requests -[Feature Request Forums](https://forums.rocket.chat/c/feature-requests) are used to suggest, discuss and upvote feature suggestions. +[Feature Request Forums](https://forums.rocket.chat/c/feature-requests) are used to suggest, discuss and upvote feature suggestions. ### Stack Overflow @@ -378,7 +389,7 @@ We are developing the APIs based on the competition, so stay tuned and you will ## Documentation -Checkout [Rocket.Chat documentation](https://rocket.chat/docs/). +Check out [Rocket.Chat documentation](https://rocket.chat/docs/). ## License @@ -399,9 +410,17 @@ Now just clone and start the app: ```sh git clone https://github.com/RocketChat/Rocket.Chat.git cd Rocket.Chat +meteor npm install meteor npm start ``` +In order to debug the server part use [meteor debugging](https://docs.meteor.com/commandline.html#meteordebug). You should use Chrome for best debugging experience: + +```sh +meteor debug +``` +You'll find a nodejs icon in the developer console. + If you are not a developer and just want to run the server - see [deployment methods](https://rocket.chat/docs/installation/paas-deployments/). ## Branching Model @@ -413,12 +432,12 @@ It is based on [Gitflow Workflow](http://nvie.com/posts/a-successful-git-branchi See also this [Git Workflows Comparison](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) for more details. ## Translations -We are experimenting [Lingohub](https://translate.lingohub.com/engelgabriel/rocket-dot-chat/dashboard). +We are experimenting with [Lingohub](https://translate.lingohub.com/rocketchat/dashboard). If you want to help, send an email to support at rocket.chat to be invited to the translation project. ## How to Contribute -Already a JavaScript developer? Familiar with Meteor? [Pick an issue](https://github.com/RocketChat/Rocket.Chat/labels/contrib%3A%20easy), push a PR and instantly become a member of Rocket.Chat's international contributors community. +Already a JavaScript developer? Familiar with Meteor? [Pick an issue](https://github.com/RocketChat/Rocket.Chat/labels/contrib%3A%20easy), push a PR and instantly become a member of Rocket.Chat's international contributors community. For more information, check out our [Contributing Guide](.github/CONTRIBUTING.md) and our [Official Documentation for Contributors](https://rocket.chat/docs/contributing/). A lot of work has already gone into Rocket.Chat, but we have much bigger plans for it! @@ -438,9 +457,9 @@ Thanks to our core team [Sing Li](https://github.com/Sing-Li), and to hundreds of awesome [contributors](https://github.com/RocketChat/Rocket.Chat/graphs/contributors). -![Emoji One](https://cloud.githubusercontent.com/assets/1986378/24772858/47290a70-1ae9-11e7-9a5a-2913d0002c94.png) +![JoyPixels](https://i.imgur.com/OrhYvLe.png) -Emoji provided free by [Emoji One](http://emojione.com) +Emoji provided graciously by [JoyPixels](https://www.joypixels.com/) ![BrowserStack](https://cloud.githubusercontent.com/assets/1986378/24772879/57d57b88-1ae9-11e7-98b4-4af824b47933.png) @@ -450,11 +469,9 @@ Testing with [BrowserStack](https://www.browserstack.com) # Donate -Rocket.Chat will be free forever, but you can help us speed-up the development! - -[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=ZL94ZE6LGVUSN) +Rocket.Chat will be free forever, but you can help us speed up the development! -[![Bitcoins](https://github.com/RocketChat/Rocket.Chat.Docs/blob/master/1.%20Contributing/Donating/coinbase.png?raw=true)](https://www.coinbase.com/checkouts/ac2fa967efca7f6fc1201d46bdccb875) +[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=9MT88JJ9X4A6U&source=url) [BountySource](https://www.bountysource.com/teams/rocketchat) diff --git a/app.json b/app.json index ea31755dbbba..1513ba1382a8 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "name": "Rocket.Chat", "description": "Have your own open-source Slack-like online chat platform.", "repository": "https://github.com/RocketChat/Rocket.Chat", - "logo": "https://raw.githubusercontent.com/RocketChat/Rocket.Chat/master/public/images/logo/512x512.png?v=3", + "logo": "https://raw.githubusercontent.com/RocketChat/Rocket.Chat/master/public/images/logo/1024x1024.png", "keywords": ["meteor", "social", "community", "chat"], "website": "https://rocket.chat", "env": { diff --git a/packages/rocketchat-2fa/README.md b/app/2fa/README.md similarity index 100% rename from packages/rocketchat-2fa/README.md rename to app/2fa/README.md diff --git a/packages/rocketchat-2fa/client/TOTPPassword.js b/app/2fa/client/TOTPPassword.js similarity index 90% rename from packages/rocketchat-2fa/client/TOTPPassword.js rename to app/2fa/client/TOTPPassword.js index d8bf6b114bb7..7a26863faadf 100644 --- a/packages/rocketchat-2fa/client/TOTPPassword.js +++ b/app/2fa/client/TOTPPassword.js @@ -1,5 +1,10 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import toastr from 'toastr'; +import { modal } from '../../ui-utils'; +import { t } from '../../utils'; + function reportError(error, callback) { if (callback) { callback(error); diff --git a/app/2fa/client/accountSecurity.html b/app/2fa/client/accountSecurity.html new file mode 100644 index 000000000000..c6d20854f8e9 --- /dev/null +++ b/app/2fa/client/accountSecurity.html @@ -0,0 +1,47 @@ + diff --git a/packages/rocketchat-2fa/client/accountSecurity.js b/app/2fa/client/accountSecurity.js similarity index 92% rename from packages/rocketchat-2fa/client/accountSecurity.js rename to app/2fa/client/accountSecurity.js index 6b9c15d0ec9a..027da271a65b 100644 --- a/packages/rocketchat-2fa/client/accountSecurity.js +++ b/app/2fa/client/accountSecurity.js @@ -1,6 +1,13 @@ +import { Meteor } from 'meteor/meteor'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Template } from 'meteor/templating'; import toastr from 'toastr'; import qrcode from 'yaqrcode'; +import { modal } from '../../ui-utils'; +import { settings } from '../../settings'; +import { t } from '../../utils'; + window.qrcode = qrcode; Template.accountSecurity.helpers({ @@ -21,7 +28,7 @@ Template.accountSecurity.helpers({ return Template.instance().state.get() === 'registering'; }, isAllowed() { - return RocketChat.settings.get('Accounts_TwoFactorAuthentication_Enabled'); + return settings.get('Accounts_TwoFactorAuthentication_Enabled'); }, codesRemaining() { if (Template.instance().codesRemaining.get()) { diff --git a/app/2fa/client/index.js b/app/2fa/client/index.js new file mode 100644 index 000000000000..c5b1485a073b --- /dev/null +++ b/app/2fa/client/index.js @@ -0,0 +1,3 @@ +import './accountSecurity.html'; +import './accountSecurity'; +import './TOTPPassword'; diff --git a/app/2fa/server/index.js b/app/2fa/server/index.js new file mode 100644 index 000000000000..e5d5ab3fc445 --- /dev/null +++ b/app/2fa/server/index.js @@ -0,0 +1,7 @@ +import './startup/settings'; +import './methods/checkCodesRemaining'; +import './methods/disable'; +import './methods/enable'; +import './methods/regenerateCodes'; +import './methods/validateTempToken'; +import './loginHandler'; diff --git a/packages/rocketchat-2fa/server/lib/totp.js b/app/2fa/server/lib/totp.js similarity index 78% rename from packages/rocketchat-2fa/server/lib/totp.js rename to app/2fa/server/lib/totp.js index f245d209f5d3..fcdf46f3bbdb 100644 --- a/packages/rocketchat-2fa/server/lib/totp.js +++ b/app/2fa/server/lib/totp.js @@ -1,6 +1,11 @@ +import { SHA256 } from 'meteor/sha'; +import { Random } from 'meteor/random'; import speakeasy from 'speakeasy'; -RocketChat.TOTP = { +import { Users } from '../../../models'; +import { settings } from '../../../settings'; + +export const TOTP = { generateSecret() { return speakeasy.generateSecret(); }, @@ -22,14 +27,14 @@ RocketChat.TOTP = { backupTokens.splice(usedCode, 1); // mark the code as used (remove it from the list) - RocketChat.models.Users.update2FABackupCodesByUserId(userId, backupTokens); + Users.update2FABackupCodesByUserId(userId, backupTokens); return true; } return false; } - const maxDelta = RocketChat.settings.get('Accounts_TwoFactorAuthentication_MaxDelta'); + const maxDelta = settings.get('Accounts_TwoFactorAuthentication_MaxDelta'); if (maxDelta) { const verifiedDelta = speakeasy.totp.verifyDelta({ secret, diff --git a/app/2fa/server/loginHandler.js b/app/2fa/server/loginHandler.js new file mode 100644 index 000000000000..a70f582f946b --- /dev/null +++ b/app/2fa/server/loginHandler.js @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; + +import { TOTP } from './lib/totp'; +import { settings } from '../../settings'; +import { callbacks } from '../../callbacks'; + +Accounts.registerLoginHandler('totp', function(options) { + if (!options.totp || !options.totp.code) { + return; + } + + return Accounts._runLoginHandlers(this, options.totp.login); +}); + +callbacks.add('onValidateLogin', (login) => { + if (!settings.get('Accounts_TwoFactorAuthentication_Enabled')) { + return; + } + + if (login.type === 'password' && login.user.services && login.user.services.totp && login.user.services.totp.enabled === true) { + const { totp } = login.methodArguments[0]; + + if (!totp || !totp.code) { + throw new Meteor.Error('totp-required', 'TOTP Required'); + } + + const verified = TOTP.verify({ + secret: login.user.services.totp.secret, + token: totp.code, + userId: login.user._id, + backupTokens: login.user.services.totp.hashedBackup, + }); + + if (verified !== true) { + throw new Meteor.Error('totp-invalid', 'TOTP Invalid'); + } + } +}, callbacks.priority.MEDIUM, '2fa'); diff --git a/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js b/app/2fa/server/methods/checkCodesRemaining.js similarity index 89% rename from packages/rocketchat-2fa/server/methods/checkCodesRemaining.js rename to app/2fa/server/methods/checkCodesRemaining.js index 5deec43f7d19..63222c87da75 100644 --- a/packages/rocketchat-2fa/server/methods/checkCodesRemaining.js +++ b/app/2fa/server/methods/checkCodesRemaining.js @@ -1,3 +1,5 @@ +import { Meteor } from 'meteor/meteor'; + Meteor.methods({ '2fa:checkCodesRemaining'() { if (!Meteor.userId()) { diff --git a/app/2fa/server/methods/disable.js b/app/2fa/server/methods/disable.js new file mode 100644 index 000000000000..fe6e554305dd --- /dev/null +++ b/app/2fa/server/methods/disable.js @@ -0,0 +1,27 @@ +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../../models'; +import { TOTP } from '../lib/totp'; + +Meteor.methods({ + '2fa:disable'(code) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + const verified = TOTP.verify({ + secret: user.services.totp.secret, + token: code, + userId: Meteor.userId(), + backupTokens: user.services.totp.hashedBackup, + }); + + if (!verified) { + return false; + } + + return Users.disable2FAByUserId(Meteor.userId()); + }, +}); diff --git a/app/2fa/server/methods/enable.js b/app/2fa/server/methods/enable.js new file mode 100644 index 000000000000..ef34662436e6 --- /dev/null +++ b/app/2fa/server/methods/enable.js @@ -0,0 +1,23 @@ +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../../models'; +import { TOTP } from '../lib/totp'; + +Meteor.methods({ + '2fa:enable'() { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + const secret = TOTP.generateSecret(); + + Users.disable2FAAndSetTempSecretByUserId(Meteor.userId(), secret.base32); + + return { + secret: secret.base32, + url: TOTP.generateOtpauthURL(secret, user.username), + }; + }, +}); diff --git a/app/2fa/server/methods/regenerateCodes.js b/app/2fa/server/methods/regenerateCodes.js new file mode 100644 index 000000000000..bfdc8d955d78 --- /dev/null +++ b/app/2fa/server/methods/regenerateCodes.js @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../../models'; +import { TOTP } from '../lib/totp'; + +Meteor.methods({ + '2fa:regenerateCodes'(userToken) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.enabled) { + throw new Meteor.Error('invalid-totp'); + } + + const verified = TOTP.verify({ + secret: user.services.totp.secret, + token: userToken, + userId: Meteor.userId(), + backupTokens: user.services.totp.hashedBackup, + }); + + if (verified) { + const { codes, hashedCodes } = TOTP.generateCodes(); + + Users.update2FABackupCodesByUserId(Meteor.userId(), hashedCodes); + return { codes }; + } + }, +}); diff --git a/app/2fa/server/methods/validateTempToken.js b/app/2fa/server/methods/validateTempToken.js new file mode 100644 index 000000000000..71565b0d42e3 --- /dev/null +++ b/app/2fa/server/methods/validateTempToken.js @@ -0,0 +1,30 @@ +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../../models'; +import { TOTP } from '../lib/totp'; + +Meteor.methods({ + '2fa:validateTempToken'(userToken) { + if (!Meteor.userId()) { + throw new Meteor.Error('not-authorized'); + } + + const user = Meteor.user(); + + if (!user.services || !user.services.totp || !user.services.totp.tempSecret) { + throw new Meteor.Error('invalid-totp'); + } + + const verified = TOTP.verify({ + secret: user.services.totp.tempSecret, + token: userToken, + }); + + if (verified) { + const { codes, hashedCodes } = TOTP.generateCodes(); + + Users.enable2FAAndSetSecretAndCodesByUserId(Meteor.userId(), user.services.totp.tempSecret, hashedCodes); + return { codes }; + } + }, +}); diff --git a/app/2fa/server/startup/settings.js b/app/2fa/server/startup/settings.js new file mode 100644 index 000000000000..51ccf2ac68f3 --- /dev/null +++ b/app/2fa/server/startup/settings.js @@ -0,0 +1,19 @@ +import { settings } from '../../../settings'; + +settings.addGroup('Accounts', function() { + this.section('Two Factor Authentication', function() { + this.add('Accounts_TwoFactorAuthentication_Enabled', true, { + type: 'boolean', + public: true, + }); + this.add('Accounts_TwoFactorAuthentication_MaxDelta', 1, { + type: 'int', + public: true, + i18nLabel: 'Accounts_TwoFactorAuthentication_MaxDelta', + enableQuery: { + _id: 'Accounts_TwoFactorAuthentication_Enabled', + value: true, + }, + }); + }); +}); diff --git a/packages/rocketchat-accounts/README.md b/app/accounts/README.md similarity index 100% rename from packages/rocketchat-accounts/README.md rename to app/accounts/README.md diff --git a/app/accounts/index.js b/app/accounts/index.js new file mode 100644 index 000000000000..ca39cd0df4b1 --- /dev/null +++ b/app/accounts/index.js @@ -0,0 +1 @@ +export * from './server/index'; diff --git a/packages/rocketchat-accounts/server/config.js b/app/accounts/server/config.js similarity index 100% rename from packages/rocketchat-accounts/server/config.js rename to app/accounts/server/config.js diff --git a/packages/rocketchat-accounts/server/index.js b/app/accounts/server/index.js similarity index 100% rename from packages/rocketchat-accounts/server/index.js rename to app/accounts/server/index.js diff --git a/packages/rocketchat-action-links/README.md b/app/action-links/README.md similarity index 100% rename from packages/rocketchat-action-links/README.md rename to app/action-links/README.md diff --git a/app/action-links/both/lib/actionLinks.js b/app/action-links/both/lib/actionLinks.js new file mode 100644 index 000000000000..c87c712e079b --- /dev/null +++ b/app/action-links/both/lib/actionLinks.js @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; + +import { Messages, Subscriptions } from '../../../models'; + +// Action Links namespace creation. +export const actionLinks = { + actions: {}, + register(name, funct) { + actionLinks.actions[name] = funct; + }, + getMessage(name, messageId) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'actionLinks.getMessage' }); + } + + const message = Messages.findOne({ _id: messageId }); + if (!message) { + throw new Meteor.Error('error-invalid-message', 'Invalid message', { function: 'actionLinks.getMessage' }); + } + + const subscription = Subscriptions.findOne({ + rid: message.rid, + 'u._id': userId, + }); + if (!subscription) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { function: 'actionLinks.getMessage' }); + } + + if (!message.actionLinks || !message.actionLinks[name]) { + throw new Meteor.Error('error-invalid-actionlink', 'Invalid action link', { function: 'actionLinks.getMessage' }); + } + + return message; + }, +}; diff --git a/app/action-links/client/index.js b/app/action-links/client/index.js new file mode 100644 index 000000000000..f49166a5c811 --- /dev/null +++ b/app/action-links/client/index.js @@ -0,0 +1,7 @@ +import { actionLinks } from '../both/lib/actionLinks'; +import './lib/actionLinks'; +import './init'; + +export { + actionLinks, +}; diff --git a/app/action-links/client/init.js b/app/action-links/client/init.js new file mode 100644 index 000000000000..2865be4279c9 --- /dev/null +++ b/app/action-links/client/init.js @@ -0,0 +1,33 @@ +import { Blaze } from 'meteor/blaze'; +import { Template } from 'meteor/templating'; + +import { handleError } from '../../utils'; +import { fireGlobalEvent, Layout } from '../../ui-utils'; +import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; +import { actionLinks } from '../both/lib/actionLinks'; + + +Template.room.events({ + 'click .action-link'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + const data = Blaze.getData(event.currentTarget); + const { msg } = messageArgs(data); + if (Layout.isEmbedded()) { + return fireGlobalEvent('click-action-link', { + actionlink: $(event.currentTarget).data('actionlink'), + value: msg._id, + message: msg, + }); + } + + if (msg._id) { + actionLinks.run($(event.currentTarget).data('actionlink'), msg._id, instance, (err) => { + if (err) { + handleError(err); + } + }); + } + }, +}); diff --git a/app/action-links/client/lib/actionLinks.js b/app/action-links/client/lib/actionLinks.js new file mode 100644 index 000000000000..4391eda94afb --- /dev/null +++ b/app/action-links/client/lib/actionLinks.js @@ -0,0 +1,27 @@ +import { Meteor } from 'meteor/meteor'; + +import { handleError } from '../../../utils'; +import { actionLinks } from '../../both/lib/actionLinks'; +// Action Links Handler. This method will be called off the client. + +actionLinks.run = (name, messageId, instance) => { + const message = actionLinks.getMessage(name, messageId); + + const actionLink = message.actionLinks[name]; + + let ranClient = false; + + if (actionLinks && actionLinks.actions && actionLinks.actions[actionLink.method_id]) { + // run just on client side + actionLinks.actions[actionLink.method_id](message, actionLink.params, instance); + + ranClient = true; + } + + // and run on server side + Meteor.call('actionLinkHandler', name, messageId, (err) => { + if (err && !ranClient) { + handleError(err); + } + }); +}; diff --git a/packages/rocketchat-action-links/client/stylesheets/actionLinks.css b/app/action-links/client/stylesheets/actionLinks.css similarity index 100% rename from packages/rocketchat-action-links/client/stylesheets/actionLinks.css rename to app/action-links/client/stylesheets/actionLinks.css diff --git a/app/action-links/index.js b/app/action-links/index.js new file mode 100644 index 000000000000..a67eca871efb --- /dev/null +++ b/app/action-links/index.js @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; + +if (Meteor.isClient) { + module.exports = require('./client/index.js'); +} +if (Meteor.isServer) { + module.exports = require('./server/index.js'); +} diff --git a/app/action-links/server/actionLinkHandler.js b/app/action-links/server/actionLinkHandler.js new file mode 100644 index 000000000000..067f727e3dda --- /dev/null +++ b/app/action-links/server/actionLinkHandler.js @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; + +import { actionLinks } from '../both/lib/actionLinks'; +// Action Links Handler. This method will be called off the client. + +Meteor.methods({ + actionLinkHandler(name, messageId) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'actionLinkHandler' }); + } + + const message = actionLinks.getMessage(name, messageId); + + const actionLink = message.actionLinks[name]; + + actionLinks.actions[actionLink.method_id](message, actionLink.params); + }, +}); diff --git a/app/action-links/server/index.js b/app/action-links/server/index.js new file mode 100644 index 000000000000..b1c484f79888 --- /dev/null +++ b/app/action-links/server/index.js @@ -0,0 +1,6 @@ +import { actionLinks } from '../both/lib/actionLinks'; +import './actionLinkHandler'; + +export { + actionLinks, +}; diff --git a/packages/rocketchat-analytics/README.md b/app/analytics/README.md similarity index 100% rename from packages/rocketchat-analytics/README.md rename to app/analytics/README.md diff --git a/app/analytics/client/index.js b/app/analytics/client/index.js new file mode 100644 index 000000000000..ec016dc3b595 --- /dev/null +++ b/app/analytics/client/index.js @@ -0,0 +1,2 @@ +import './loadScript'; +import './trackEvents'; diff --git a/packages/rocketchat-analytics/client/loadScript.js b/app/analytics/client/loadScript.js similarity index 76% rename from packages/rocketchat-analytics/client/loadScript.js rename to app/analytics/client/loadScript.js index 53573177267c..50d25859814f 100644 --- a/packages/rocketchat-analytics/client/loadScript.js +++ b/app/analytics/client/loadScript.js @@ -1,12 +1,18 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; +import { Template } from 'meteor/templating'; + +import { settings } from '../../settings'; + Template.body.onRendered(() => { Tracker.autorun((c) => { - const piwikUrl = RocketChat.settings.get('PiwikAnalytics_enabled') && RocketChat.settings.get('PiwikAnalytics_url'); - const piwikSiteId = piwikUrl && RocketChat.settings.get('PiwikAnalytics_siteId'); - const piwikPrependDomain = piwikUrl && RocketChat.settings.get('PiwikAnalytics_prependDomain'); - const piwikCookieDomain = piwikUrl && RocketChat.settings.get('PiwikAnalytics_cookieDomain'); - const piwikDomains = piwikUrl && RocketChat.settings.get('PiwikAnalytics_domains'); - const piwikAdditionalTracker = piwikUrl && RocketChat.settings.get('PiwikAdditionalTrackers'); - const googleId = RocketChat.settings.get('GoogleAnalytics_enabled') && RocketChat.settings.get('GoogleAnalytics_ID'); + const piwikUrl = settings.get('PiwikAnalytics_enabled') && settings.get('PiwikAnalytics_url'); + const piwikSiteId = piwikUrl && settings.get('PiwikAnalytics_siteId'); + const piwikPrependDomain = piwikUrl && settings.get('PiwikAnalytics_prependDomain'); + const piwikCookieDomain = piwikUrl && settings.get('PiwikAnalytics_cookieDomain'); + const piwikDomains = piwikUrl && settings.get('PiwikAnalytics_domains'); + const piwikAdditionalTracker = piwikUrl && settings.get('PiwikAdditionalTrackers'); + const googleId = settings.get('GoogleAnalytics_enabled') && settings.get('GoogleAnalytics_ID'); if (piwikSiteId || googleId) { c.stop(); diff --git a/app/analytics/client/trackEvents.js b/app/analytics/client/trackEvents.js new file mode 100644 index 000000000000..cc86418dca62 --- /dev/null +++ b/app/analytics/client/trackEvents.js @@ -0,0 +1,151 @@ +import { Meteor } from 'meteor/meteor'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Tracker } from 'meteor/tracker'; + +import { settings } from '../../settings'; +import { callbacks } from '../../callbacks'; +import { ChatRoom } from '../../models'; + +function trackEvent(category, action, label) { + if (window._paq) { + window._paq.push(['trackEvent', category, action, label]); + } + if (window.ga) { + window.ga('send', 'event', category, action, label); + } +} + +if (!window._paq || window.ga) { + // Trigger the trackPageView manually as the page views are only loaded when the loadScript.js code is executed + FlowRouter.triggers.enter([(route) => { + if (window._paq) { + const http = location.protocol; + const slashes = http.concat('//'); + const host = slashes.concat(window.location.hostname); + window._paq.push(['setCustomUrl', host + route.path]); + window._paq.push(['trackPageView']); + } + if (window.ga) { + window.ga('send', 'pageview', route.path); + } + }]); + + // Login page has manual switches + callbacks.add('loginPageStateChange', (state) => { + trackEvent('Navigation', 'Login Page State Change', state); + }, callbacks.priority.MEDIUM, 'analytics-login-state-change'); + + // Messsages + callbacks.add('afterSaveMessage', (message) => { + if ((window._paq || window.ga) && settings.get('Analytics_features_messages')) { + const room = ChatRoom.findOne({ _id: message.rid }); + trackEvent('Message', 'Send', `${ room.name } (${ room._id })`); + } + }, 2000, 'trackEvents'); + + // Rooms + callbacks.add('afterCreateChannel', (owner, room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Create', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-after-create-channel'); + + callbacks.add('roomNameChanged', (room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Changed Name', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-room-name-changed'); + + callbacks.add('roomTopicChanged', (room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Changed Topic', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-room-topic-changed'); + + callbacks.add('roomAnnouncementChanged', (room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Changed Announcement', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-room-announcement-changed'); + + callbacks.add('roomTypeChanged', (room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Changed Room Type', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-room-type-changed'); + + callbacks.add('archiveRoom', (room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Archived', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-archive-room'); + + callbacks.add('unarchiveRoom', (room) => { + if (settings.get('Analytics_features_rooms')) { + trackEvent('Room', 'Unarchived', `${ room.name } (${ room._id })`); + } + }, callbacks.priority.MEDIUM, 'analytics-unarchive-room'); + + // Users + // Track logins and associate user ids with piwik + (() => { + let oldUserId = null; + + Tracker.autorun(() => { + const newUserId = Meteor.userId(); + if (oldUserId === null && newUserId) { + if (window._paq && settings.get('Analytics_features_users')) { + trackEvent('User', 'Login', newUserId); + window._paq.push(['setUserId', newUserId]); + } + } else if (newUserId === null && oldUserId) { + if (window._paq && settings.get('Analytics_features_users')) { + trackEvent('User', 'Logout', oldUserId); + } + } + oldUserId = Meteor.userId(); + }); + })(); + + callbacks.add('userRegistered', () => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Registered'); + } + }, callbacks.priority.MEDIUM, 'piwik-user-resitered'); + + callbacks.add('usernameSet', () => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Username Set'); + } + }, callbacks.priority.MEDIUM, 'piweik-username-set'); + + callbacks.add('userPasswordReset', () => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Reset Password'); + } + }, callbacks.priority.MEDIUM, 'piwik-user-password-reset'); + + callbacks.add('userConfirmationEmailRequested', () => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Confirmation Email Requested'); + } + }, callbacks.priority.MEDIUM, 'piwik-user-confirmation-email-requested'); + + callbacks.add('userForgotPasswordEmailRequested', () => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Forgot Password Email Requested'); + } + }, callbacks.priority.MEDIUM, 'piwik-user-forgot-password-email-requested'); + + callbacks.add('userStatusManuallySet', (status) => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Status Manually Changed', status); + } + }, callbacks.priority.MEDIUM, 'analytics-user-status-manually-set'); + + callbacks.add('userAvatarSet', (service) => { + if (settings.get('Analytics_features_users')) { + trackEvent('User', 'Avatar Changed', service); + } + }, callbacks.priority.MEDIUM, 'analytics-user-avatar-set'); +} diff --git a/app/analytics/server/index.js b/app/analytics/server/index.js new file mode 100644 index 000000000000..97097791afdc --- /dev/null +++ b/app/analytics/server/index.js @@ -0,0 +1 @@ +import './settings'; diff --git a/app/analytics/server/settings.js b/app/analytics/server/settings.js new file mode 100644 index 000000000000..932e834cdb6e --- /dev/null +++ b/app/analytics/server/settings.js @@ -0,0 +1,87 @@ +import { settings } from '../../settings'; + +settings.addGroup('Analytics', function addSettings() { + this.section('Piwik', function() { + const enableQuery = { _id: 'PiwikAnalytics_enabled', value: true }; + this.add('PiwikAnalytics_enabled', false, { + type: 'boolean', + public: true, + i18nLabel: 'Enable', + }); + this.add('PiwikAnalytics_url', '', { + type: 'string', + public: true, + i18nLabel: 'URL', + enableQuery, + }); + this.add('PiwikAnalytics_siteId', '', { + type: 'string', + public: true, + i18nLabel: 'Client_ID', + enableQuery, + }); + this.add('PiwikAdditionalTrackers', '', { + type: 'string', + multiline: true, + public: true, + i18nLabel: 'PiwikAdditionalTrackers', + enableQuery, + }); + this.add('PiwikAnalytics_prependDomain', false, { + type: 'boolean', + public: true, + i18nLabel: 'PiwikAnalytics_prependDomain', + enableQuery, + }); + this.add('PiwikAnalytics_cookieDomain', false, { + type: 'boolean', + public: true, + i18nLabel: 'PiwikAnalytics_cookieDomain', + enableQuery, + }); + this.add('PiwikAnalytics_domains', '', { + type: 'string', + multiline: true, + public: true, + i18nLabel: 'PiwikAnalytics_domains', + enableQuery, + }); + }); + + this.section('Analytics_Google', function() { + const enableQuery = { _id: 'GoogleAnalytics_enabled', value: true }; + this.add('GoogleAnalytics_enabled', false, { + type: 'boolean', + public: true, + i18nLabel: 'Enable', + }); + + this.add('GoogleAnalytics_ID', '', { + type: 'string', + public: true, + i18nLabel: 'Analytics_Google_id', + enableQuery, + }); + }); + + this.section('Analytics_features_enabled', function addFeaturesEnabledSettings() { + this.add('Analytics_features_messages', true, { + type: 'boolean', + public: true, + i18nLabel: 'Messages', + i18nDescription: 'Analytics_features_messages_Description', + }); + this.add('Analytics_features_rooms', true, { + type: 'boolean', + public: true, + i18nLabel: 'Rooms', + i18nDescription: 'Analytics_features_rooms_Description', + }); + this.add('Analytics_features_users', true, { + type: 'boolean', + public: true, + i18nLabel: 'Users', + i18nDescription: 'Analytics_features_users_Description', + }); + }); +}); diff --git a/app/api/index.js b/app/api/index.js new file mode 100644 index 000000000000..ca39cd0df4b1 --- /dev/null +++ b/app/api/index.js @@ -0,0 +1 @@ +export * from './server/index'; diff --git a/app/api/server/api.js b/app/api/server/api.js new file mode 100644 index 000000000000..9e42911baed4 --- /dev/null +++ b/app/api/server/api.js @@ -0,0 +1,573 @@ +import { Meteor } from 'meteor/meteor'; +import { DDPCommon } from 'meteor/ddp-common'; +import { DDP } from 'meteor/ddp'; +import { Accounts } from 'meteor/accounts-base'; +import { Restivus } from 'meteor/nimble:restivus'; +import { RateLimiter } from 'meteor/rate-limit'; +import _ from 'underscore'; + +import { Logger } from '../../logger'; +import { settings } from '../../settings'; +import { metrics } from '../../metrics'; +import { hasPermission, hasAllPermission } from '../../authorization'; +import { getDefaultUserFields } from '../../utils/server/functions/getDefaultUserFields'; + + +const logger = new Logger('API', {}); +const rateLimiterDictionary = {}; +const defaultRateLimiterOptions = { + numRequestsAllowed: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default'), + intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), +}; + +export let API = {}; + +class APIClass extends Restivus { + constructor(properties) { + super(properties); + this.authMethods = []; + this.fieldSeparator = '.'; + this.defaultFieldsToExclude = { + joinCode: 0, + members: 0, + importIds: 0, + e2e: 0, + }; + this.limitedUserFieldsToExclude = { + avatarOrigin: 0, + emails: 0, + phone: 0, + statusConnection: 0, + createdAt: 0, + lastLogin: 0, + services: 0, + requirePasswordChange: 0, + requirePasswordChangeReason: 0, + roles: 0, + statusDefault: 0, + _updatedAt: 0, + customFields: 0, + settings: 0, + }; + this.limitedUserFieldsToExcludeIfIsPrivilegedUser = { + services: 0, + }; + } + + hasHelperMethods() { + return API.helperMethods.size !== 0; + } + + getHelperMethods() { + return API.helperMethods; + } + + getHelperMethod(name) { + return API.helperMethods.get(name); + } + + addAuthMethod(method) { + this.authMethods.push(method); + } + + success(result = {}) { + if (_.isObject(result)) { + result.success = true; + } + + result = { + statusCode: 200, + body: result, + }; + + logger.debug('Success', result); + + return result; + } + + failure(result, errorType, stack) { + if (_.isObject(result)) { + result.success = false; + } else { + result = { + success: false, + error: result, + stack, + }; + + if (errorType) { + result.errorType = errorType; + } + } + + result = { + statusCode: 400, + body: result, + }; + + logger.debug('Failure', result); + + return result; + } + + notFound(msg) { + return { + statusCode: 404, + body: { + success: false, + error: msg || 'Resource not found', + }, + }; + } + + internalError(msg) { + return { + statusCode: 500, + body: { + success: false, + error: msg || 'Internal error occured', + }, + }; + } + + unauthorized(msg) { + return { + statusCode: 403, + body: { + success: false, + error: msg || 'unauthorized', + }, + }; + } + + tooManyRequests(msg) { + return { + statusCode: 429, + body: { + success: false, + error: msg || 'Too many requests', + }, + }; + } + + reloadRoutesToRefreshRateLimiter() { + const { version } = this._config; + this._routes.forEach((route) => { + const shouldAddRateLimitToRoute = (typeof route.options.rateLimiterOptions === 'object' || route.options.rateLimiterOptions === undefined) && Boolean(version) && !process.env.TEST_MODE && Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS); + if (shouldAddRateLimitToRoute) { + this.addRateLimiterRuleForRoutes({ + routes: [route.path], + rateLimiterOptions: route.options.rateLimiterOptions || defaultRateLimiterOptions, + endpoints: Object.keys(route.endpoints).filter((endpoint) => endpoint !== 'options'), + apiVersion: version, + }); + } + }); + } + + addRateLimiterRuleForRoutes({ routes, rateLimiterOptions, endpoints, apiVersion }) { + if (!rateLimiterOptions.numRequestsAllowed) { + throw new Meteor.Error('You must set "numRequestsAllowed" property in rateLimiter for REST API endpoint'); + } + if (!rateLimiterOptions.intervalTimeInMS) { + throw new Meteor.Error('You must set "intervalTimeInMS" property in rateLimiter for REST API endpoint'); + } + const nameRoute = (route) => { + const routeActions = Array.isArray(endpoints) ? endpoints : Object.keys(endpoints); + return routeActions.map((endpoint) => `/api/${ apiVersion }/${ route }${ endpoint }`); + }; + const addRateLimitRuleToEveryRoute = (routes) => { + routes.forEach((route) => { + rateLimiterDictionary[route] = { + rateLimiter: new RateLimiter(), + options: rateLimiterOptions, + }; + const rateLimitRule = { + IPAddr: (input) => input, + route, + }; + rateLimiterDictionary[route].rateLimiter.addRule(rateLimitRule, rateLimiterOptions.numRequestsAllowed, rateLimiterOptions.intervalTimeInMS); + }); + }; + routes + .map(nameRoute) + .map(addRateLimitRuleToEveryRoute); + } + + addRoute(routes, options, endpoints) { + // Note: required if the developer didn't provide options + if (typeof endpoints === 'undefined') { + endpoints = options; + options = {}; + } + + let shouldVerifyPermissions; + + if (!_.isArray(options.permissionsRequired)) { + options.permissionsRequired = undefined; + shouldVerifyPermissions = false; + } else { + shouldVerifyPermissions = !!options.permissionsRequired.length; + } + + + // Allow for more than one route using the same option and endpoints + if (!_.isArray(routes)) { + routes = [routes]; + } + const { version } = this._config; + const shouldAddRateLimitToRoute = (typeof options.rateLimiterOptions === 'object' || options.rateLimiterOptions === undefined) && Boolean(version) && !process.env.TEST_MODE && Boolean(defaultRateLimiterOptions.numRequestsAllowed && defaultRateLimiterOptions.intervalTimeInMS); + if (shouldAddRateLimitToRoute) { + this.addRateLimiterRuleForRoutes({ + routes, + rateLimiterOptions: options.rateLimiterOptions || defaultRateLimiterOptions, + endpoints, + apiVersion: version, + }); + } + routes.forEach((route) => { + // Note: This is required due to Restivus calling `addRoute` in the constructor of itself + Object.keys(endpoints).forEach((method) => { + if (typeof endpoints[method] === 'function') { + endpoints[method] = { action: endpoints[method] }; + } + // Add a try/catch for each endpoint + const originalAction = endpoints[method].action; + endpoints[method].action = function _internalRouteActionHandler() { + const rocketchatRestApiEnd = metrics.rocketchatRestApi.startTimer({ + method, + version, + user_agent: this.request.headers['user-agent'], + entrypoint: route, + }); + + logger.debug(`${ this.request.method.toUpperCase() }: ${ this.request.url }`); + const requestIp = this.request.headers['x-forwarded-for'] || this.request.connection.remoteAddress || this.request.socket.remoteAddress || this.request.connection.socket.remoteAddress; + const objectForRateLimitMatch = { + IPAddr: requestIp, + route: `${ this.request.route }${ this.request.method.toLowerCase() }`, + }; + let result; + try { + const shouldVerifyRateLimit = rateLimiterDictionary.hasOwnProperty(objectForRateLimitMatch.route) + && settings.get('API_Enable_Rate_Limiter') === true + && (process.env.NODE_ENV !== 'development' || settings.get('API_Enable_Rate_Limiter_Dev') === true) + && !(this.userId && hasPermission(this.userId, 'api-bypass-rate-limit')); + + if (shouldVerifyRateLimit) { + rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.increment(objectForRateLimitMatch); + const attemptResult = rateLimiterDictionary[objectForRateLimitMatch.route].rateLimiter.check(objectForRateLimitMatch); + const timeToResetAttempsInSeconds = Math.ceil(attemptResult.timeToReset / 1000); + this.response.setHeader('X-RateLimit-Limit', rateLimiterDictionary[objectForRateLimitMatch.route].options.numRequestsAllowed); + this.response.setHeader('X-RateLimit-Remaining', attemptResult.numInvocationsLeft); + this.response.setHeader('X-RateLimit-Reset', new Date().getTime() + attemptResult.timeToReset); + if (!attemptResult.allowed) { + throw new Meteor.Error('error-too-many-requests', `Error, too many requests. Please slow down. You must wait ${ timeToResetAttempsInSeconds } seconds before trying this endpoint again.`, { + timeToReset: attemptResult.timeToReset, + seconds: timeToResetAttempsInSeconds, + }); + } + } + + if (shouldVerifyPermissions && (!this.userId || !hasAllPermission(this.userId, options.permissionsRequired))) { + throw new Meteor.Error('error-unauthorized', 'User does not have the permissions required for this action', { + permissions: options.permissionsRequired, + }); + } + + result = originalAction.apply(this); + } catch (e) { + logger.debug(`${ method } ${ route } threw an error:`, e.stack); + + const apiMethod = { + 'error-too-many-requests': 'tooManyRequests', + 'error-unauthorized': 'unauthorized', + }[e.error] || 'failure'; + + result = API.v1[apiMethod](e.message, e.error); + } + + result = result || API.v1.success(); + + rocketchatRestApiEnd({ + status: result.statusCode, + }); + + return result; + }; + + if (this.hasHelperMethods()) { + for (const [name, helperMethod] of this.getHelperMethods()) { + endpoints[method][name] = helperMethod; + } + } + + // Allow the endpoints to make usage of the logger which respects the user's settings + endpoints[method].logger = logger; + }); + + super.addRoute(route, options, endpoints); + }); + } + + _initAuth() { + const loginCompatibility = (bodyParams) => { + // Grab the username or email that the user is logging in with + const { user, username, email, password, code } = bodyParams; + + if (password == null) { + return bodyParams; + } + + if (_.without(Object.keys(bodyParams), 'user', 'username', 'email', 'password', 'code').length > 0) { + return bodyParams; + } + + const auth = { + password, + }; + + if (typeof user === 'string') { + auth.user = user.includes('@') ? { email: user } : { username: user }; + } else if (username) { + auth.user = { username }; + } else if (email) { + auth.user = { email }; + } + + if (auth.user == null) { + return bodyParams; + } + + if (auth.password.hashed) { + auth.password = { + digest: auth.password, + algorithm: 'sha-256', + }; + } + + if (code) { + return { + totp: { + code, + login: auth, + }, + }; + } + + return auth; + }; + + const self = this; + + this.addRoute('login', { authRequired: false }, { + post() { + const args = loginCompatibility(this.bodyParams); + const getUserInfo = self.getHelperMethod('getUserInfo'); + + const invocation = new DDPCommon.MethodInvocation({ + connection: { + close() {}, + }, + }); + + let auth; + try { + auth = DDP._CurrentInvocation.withValue(invocation, () => Meteor.call('login', args)); + } catch (error) { + let e = error; + if (error.reason === 'User not found') { + e = { + error: 'Unauthorized', + reason: 'Unauthorized', + }; + } + + return { + statusCode: 401, + body: { + status: 'error', + error: e.error, + message: e.reason || e.message, + }, + }; + } + + this.user = Meteor.users.findOne({ + _id: auth.id, + }, { + fields: getDefaultUserFields(), + }); + + this.userId = this.user._id; + + const response = { + status: 'success', + data: { + userId: this.userId, + authToken: auth.token, + me: getUserInfo(this.user), + }, + }; + + const extraData = self._config.onLoggedIn && self._config.onLoggedIn.call(this); + + if (extraData != null) { + _.extend(response.data, { + extra: extraData, + }); + } + + return response; + }, + }); + + const logout = function() { + // Remove the given auth token from the user's account + const authToken = this.request.headers['x-auth-token']; + const hashedToken = Accounts._hashLoginToken(authToken); + const tokenLocation = self._config.auth.token; + const index = tokenLocation.lastIndexOf('.'); + const tokenPath = tokenLocation.substring(0, index); + const tokenFieldName = tokenLocation.substring(index + 1); + const tokenToRemove = {}; + tokenToRemove[tokenFieldName] = hashedToken; + const tokenRemovalQuery = {}; + tokenRemovalQuery[tokenPath] = tokenToRemove; + + Meteor.users.update(this.user._id, { + $pull: tokenRemovalQuery, + }); + + const response = { + status: 'success', + data: { + message: 'You\'ve been logged out!', + }, + }; + + // Call the logout hook with the authenticated user attached + const extraData = self._config.onLoggedOut && self._config.onLoggedOut.call(this); + if (extraData != null) { + _.extend(response.data, { + extra: extraData, + }); + } + return response; + }; + + /* + Add a logout endpoint to the API + After the user is logged out, the onLoggedOut hook is called (see Restfully.configure() for + adding hook). + */ + return this.addRoute('logout', { + authRequired: true, + }, { + get() { + console.warn('Warning: Default logout via GET will be removed in Restivus v1.0. Use POST instead.'); + console.warn(' See https://github.com/kahmali/meteor-restivus/issues/100'); + return logout.call(this); + }, + post: logout, + }); + } +} + +const getUserAuth = function _getUserAuth(...args) { + const invalidResults = [undefined, null, false]; + return { + token: 'services.resume.loginTokens.hashedToken', + user() { + if (this.bodyParams && this.bodyParams.payload) { + this.bodyParams = JSON.parse(this.bodyParams.payload); + } + + for (let i = 0; i < API.v1.authMethods.length; i++) { + const method = API.v1.authMethods[i]; + + if (typeof method === 'function') { + const result = method.apply(this, args); + if (!invalidResults.includes(result)) { + return result; + } + } + } + + let token; + if (this.request.headers['x-auth-token']) { + token = Accounts._hashLoginToken(this.request.headers['x-auth-token']); + } + + return { + userId: this.request.headers['x-user-id'], + token, + }; + }, + }; +}; + +API = { + helperMethods: new Map(), + getUserAuth, + ApiClass: APIClass, +}; + +const defaultOptionsEndpoint = function _defaultOptionsEndpoint() { + if (this.request.method === 'OPTIONS' && this.request.headers['access-control-request-method']) { + if (settings.get('API_Enable_CORS') === true) { + this.response.writeHead(200, { + 'Access-Control-Allow-Origin': settings.get('API_CORS_Origin'), + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, HEAD, PATCH', + 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, X-User-Id, X-Auth-Token, x-visitor-token', + }); + } else { + this.response.writeHead(405); + this.response.write('CORS not enabled. Go to "Admin > General > REST Api" to enable it.'); + } + } else { + this.response.writeHead(404); + } + this.done(); +}; + +const createApi = function _createApi(enableCors) { + if (!API.v1 || API.v1._config.enableCors !== enableCors) { + API.v1 = new APIClass({ + version: 'v1', + useDefaultAuth: true, + prettyJson: process.env.NODE_ENV === 'development', + enableCors, + defaultOptionsEndpoint, + auth: getUserAuth(), + }); + } + + if (!API.default || API.default._config.enableCors !== enableCors) { + API.default = new APIClass({ + useDefaultAuth: true, + prettyJson: process.env.NODE_ENV === 'development', + enableCors, + defaultOptionsEndpoint, + auth: getUserAuth(), + }); + } +}; + +// also create the API immediately +createApi(!!settings.get('API_Enable_CORS')); + +// register the API to be re-created once the CORS-setting changes. +settings.get('API_Enable_CORS', (key, value) => { + createApi(value); +}); + +settings.get('API_Enable_Rate_Limiter_Limit_Time_Default', (key, value) => { + defaultRateLimiterOptions.intervalTimeInMS = value; + API.v1.reloadRoutesToRefreshRateLimiter(); +}); + +settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default', (key, value) => { + defaultRateLimiterOptions.numRequestsAllowed = value; + API.v1.reloadRoutesToRefreshRateLimiter(); +}); diff --git a/app/api/server/default/info.js b/app/api/server/default/info.js new file mode 100644 index 000000000000..7c397de09cb1 --- /dev/null +++ b/app/api/server/default/info.js @@ -0,0 +1,19 @@ +import { hasRole } from '../../../authorization'; +import { Info } from '../../../utils'; +import { API } from '../api'; + +API.default.addRoute('info', { authRequired: false }, { + get() { + const user = this.getLoggedInUser(); + + if (user && hasRole(user._id, 'admin')) { + return API.v1.success({ + info: Info, + }); + } + + return API.v1.success({ + version: Info.version, + }); + }, +}); diff --git a/packages/rocketchat-api/server/helpers/README.md b/app/api/server/helpers/README.md similarity index 100% rename from packages/rocketchat-api/server/helpers/README.md rename to app/api/server/helpers/README.md diff --git a/app/api/server/helpers/composeRoomWithLastMessage.js b/app/api/server/helpers/composeRoomWithLastMessage.js new file mode 100644 index 000000000000..8822e6d575cc --- /dev/null +++ b/app/api/server/helpers/composeRoomWithLastMessage.js @@ -0,0 +1,10 @@ +import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import { API } from '../api'; + +API.helperMethods.set('composeRoomWithLastMessage', function _composeRoomWithLastMessage(room, userId) { + if (room.lastMessage) { + const [lastMessage] = normalizeMessagesForUser([room.lastMessage], userId); + room.lastMessage = lastMessage; + } + return room; +}); diff --git a/app/api/server/helpers/deprecationWarning.js b/app/api/server/helpers/deprecationWarning.js new file mode 100644 index 000000000000..fdcc98f4b1d2 --- /dev/null +++ b/app/api/server/helpers/deprecationWarning.js @@ -0,0 +1,14 @@ +import { API } from '../api'; + +API.helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemoved, response }) { + const warningMessage = `The endpoint "${ endpoint }" is deprecated and will be removed after version ${ versionWillBeRemoved }`; + console.warn(warningMessage); + if (process.env.NODE_ENV === 'development') { + return { + warning: warningMessage, + ...response, + }; + } + + return response; +}); diff --git a/app/api/server/helpers/getLoggedInUser.js b/app/api/server/helpers/getLoggedInUser.js new file mode 100644 index 000000000000..1ce74a93e2fc --- /dev/null +++ b/app/api/server/helpers/getLoggedInUser.js @@ -0,0 +1,17 @@ +import { Accounts } from 'meteor/accounts-base'; + +import { Users } from '../../../models'; +import { API } from '../api'; + +API.helperMethods.set('getLoggedInUser', function _getLoggedInUser() { + let user; + + if (this.request.headers['x-auth-token'] && this.request.headers['x-user-id']) { + user = Users.findOne({ + _id: this.request.headers['x-user-id'], + 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(this.request.headers['x-auth-token']), + }); + } + + return user; +}); diff --git a/app/api/server/helpers/getPaginationItems.js b/app/api/server/helpers/getPaginationItems.js new file mode 100644 index 000000000000..93a19b2cbf9f --- /dev/null +++ b/app/api/server/helpers/getPaginationItems.js @@ -0,0 +1,32 @@ +// If the count query param is higher than the "API_Upper_Count_Limit" setting, then we limit that +// If the count query param isn't defined, then we set it to the "API_Default_Count" setting +// If the count is zero, then that means unlimited and is only allowed if the setting "API_Allow_Infinite_Count" is true +import { settings } from '../../../settings'; +import { API } from '../api'; + +API.helperMethods.set('getPaginationItems', function _getPaginationItems() { + const hardUpperLimit = settings.get('API_Upper_Count_Limit') <= 0 ? 100 : settings.get('API_Upper_Count_Limit'); + const defaultCount = settings.get('API_Default_Count') <= 0 ? 50 : settings.get('API_Default_Count'); + const offset = this.queryParams.offset ? parseInt(this.queryParams.offset) : 0; + let count = defaultCount; + + // Ensure count is an appropiate amount + if (typeof this.queryParams.count !== 'undefined') { + count = parseInt(this.queryParams.count); + } else { + count = defaultCount; + } + + if (count > hardUpperLimit) { + count = hardUpperLimit; + } + + if (count === 0 && !settings.get('API_Allow_Infinite_Count')) { + count = defaultCount; + } + + return { + offset, + count, + }; +}); diff --git a/app/api/server/helpers/getUserFromParams.js b/app/api/server/helpers/getUserFromParams.js new file mode 100644 index 000000000000..c97d6f34e1e9 --- /dev/null +++ b/app/api/server/helpers/getUserFromParams.js @@ -0,0 +1,27 @@ +// Convenience method, almost need to turn it into a middleware of sorts +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../../models'; +import { API } from '../api'; + +API.helperMethods.set('getUserFromParams', function _getUserFromParams() { + const doesntExist = { _doesntExist: true }; + let user; + const params = this.requestParams(); + + if (params.userId && params.userId.trim()) { + user = Users.findOneById(params.userId) || doesntExist; + } else if (params.username && params.username.trim()) { + user = Users.findOneByUsernameIgnoringCase(params.username) || doesntExist; + } else if (params.user && params.user.trim()) { + user = Users.findOneByUsernameIgnoringCase(params.user) || doesntExist; + } else { + throw new Meteor.Error('error-user-param-not-provided', 'The required "userId" or "username" param was not provided'); + } + + if (user._doesntExist) { + throw new Meteor.Error('error-invalid-user', 'The required "userId" or "username" param provided does not match any users'); + } + + return user; +}); diff --git a/app/api/server/helpers/getUserInfo.js b/app/api/server/helpers/getUserInfo.js new file mode 100644 index 000000000000..2d2daee1af34 --- /dev/null +++ b/app/api/server/helpers/getUserInfo.js @@ -0,0 +1,37 @@ +import { settings } from '../../../settings/server'; +import { getUserPreference, getURL } from '../../../utils/server'; +import { API } from '../api'; + +API.helperMethods.set('getUserInfo', function _getUserInfo(me) { + const isVerifiedEmail = () => { + if (me && me.emails && Array.isArray(me.emails)) { + return me.emails.find((email) => email.verified); + } + return false; + }; + const getUserPreferences = () => { + const defaultUserSettingPrefix = 'Accounts_Default_User_Preferences_'; + const allDefaultUserSettings = settings.get(new RegExp(`^${ defaultUserSettingPrefix }.*$`)); + + return allDefaultUserSettings.reduce((accumulator, setting) => { + const settingWithoutPrefix = setting.key.replace(defaultUserSettingPrefix, ' ').trim(); + accumulator[settingWithoutPrefix] = getUserPreference(me, settingWithoutPrefix); + return accumulator; + }, {}); + }; + const verifiedEmail = isVerifiedEmail(); + me.email = verifiedEmail ? verifiedEmail.address : undefined; + + me.avatarUrl = getURL(`/avatar/${ me.username }`, { cdn: false, full: true }); + + const userPreferences = (me.settings && me.settings.preferences) || {}; + + me.settings = { + preferences: { + ...getUserPreferences(), + ...userPreferences, + }, + }; + + return me; +}); diff --git a/app/api/server/helpers/insertUserObject.js b/app/api/server/helpers/insertUserObject.js new file mode 100644 index 000000000000..f6674720c061 --- /dev/null +++ b/app/api/server/helpers/insertUserObject.js @@ -0,0 +1,17 @@ +import { Users } from '../../../models'; +import { API } from '../api'; + +API.helperMethods.set('insertUserObject', function _addUserToObject({ object, userId }) { + const user = Users.findOneById(userId); + object.user = { }; + if (user) { + object.user = { + _id: userId, + username: user.username, + name: user.name, + }; + } + + + return object; +}); diff --git a/app/api/server/helpers/isUserFromParams.js b/app/api/server/helpers/isUserFromParams.js new file mode 100644 index 000000000000..605f598bd43d --- /dev/null +++ b/app/api/server/helpers/isUserFromParams.js @@ -0,0 +1,10 @@ +import { API } from '../api'; + +API.helperMethods.set('isUserFromParams', function _isUserFromParams() { + const params = this.requestParams(); + + return (!params.userId && !params.username && !params.user) + || (params.userId && this.userId === params.userId) + || (params.username && this.user.username === params.username) + || (params.user && this.user.username === params.user); +}); diff --git a/app/api/server/helpers/parseJsonQuery.js b/app/api/server/helpers/parseJsonQuery.js new file mode 100644 index 000000000000..d7eafe0f6da8 --- /dev/null +++ b/app/api/server/helpers/parseJsonQuery.js @@ -0,0 +1,86 @@ +import { Meteor } from 'meteor/meteor'; +import { EJSON } from 'meteor/ejson'; + +import { hasPermission } from '../../../authorization'; +import { API } from '../api'; + +API.helperMethods.set('parseJsonQuery', function _parseJsonQuery() { + let sort; + if (this.queryParams.sort) { + try { + sort = JSON.parse(this.queryParams.sort); + } catch (e) { + this.logger.warn(`Invalid sort parameter provided "${ this.queryParams.sort }":`, e); + throw new Meteor.Error('error-invalid-sort', `Invalid sort parameter provided: "${ this.queryParams.sort }"`, { helperMethod: 'parseJsonQuery' }); + } + } + + let fields; + if (this.queryParams.fields) { + try { + fields = JSON.parse(this.queryParams.fields); + } catch (e) { + this.logger.warn(`Invalid fields parameter provided "${ this.queryParams.fields }":`, e); + throw new Meteor.Error('error-invalid-fields', `Invalid fields parameter provided: "${ this.queryParams.fields }"`, { helperMethod: 'parseJsonQuery' }); + } + } + + // Verify the user's selected fields only contains ones which their role allows + if (typeof fields === 'object') { + let nonSelectableFields = Object.keys(API.v1.defaultFieldsToExclude); + if (this.request.route.includes('/v1/users.')) { + const getFields = () => Object.keys(hasPermission(this.userId, 'view-full-other-user-info') ? API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser : API.v1.limitedUserFieldsToExclude); + nonSelectableFields = nonSelectableFields.concat(getFields()); + } + + Object.keys(fields).forEach((k) => { + if (nonSelectableFields.includes(k) || nonSelectableFields.includes(k.split(API.v1.fieldSeparator)[0])) { + delete fields[k]; + } + }); + } + + // Limit the fields by default + fields = Object.assign({}, fields, API.v1.defaultFieldsToExclude); + if (this.request.route.includes('/v1/users.')) { + if (hasPermission(this.userId, 'view-full-other-user-info')) { + fields = Object.assign(fields, API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser); + } else { + fields = Object.assign(fields, API.v1.limitedUserFieldsToExclude); + } + } + + let query = {}; + if (this.queryParams.query) { + try { + query = EJSON.parse(this.queryParams.query); + } catch (e) { + this.logger.warn(`Invalid query parameter provided "${ this.queryParams.query }":`, e); + throw new Meteor.Error('error-invalid-query', `Invalid query parameter provided: "${ this.queryParams.query }"`, { helperMethod: 'parseJsonQuery' }); + } + } + + // Verify the user has permission to query the fields they are + if (typeof query === 'object') { + let nonQueryableFields = Object.keys(API.v1.defaultFieldsToExclude); + if (this.request.route.includes('/v1/users.')) { + if (hasPermission(this.userId, 'view-full-other-user-info')) { + nonQueryableFields = nonQueryableFields.concat(Object.keys(API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser)); + } else { + nonQueryableFields = nonQueryableFields.concat(Object.keys(API.v1.limitedUserFieldsToExclude)); + } + } + + Object.keys(query).forEach((k) => { + if (nonQueryableFields.includes(k) || nonQueryableFields.includes(k.split(API.v1.fieldSeparator)[0])) { + delete query[k]; + } + }); + } + + return { + sort, + fields, + query, + }; +}); diff --git a/app/api/server/helpers/requestParams.js b/app/api/server/helpers/requestParams.js new file mode 100644 index 000000000000..2883c94a727e --- /dev/null +++ b/app/api/server/helpers/requestParams.js @@ -0,0 +1,5 @@ +import { API } from '../api'; + +API.helperMethods.set('requestParams', function _requestParams() { + return ['POST', 'PUT'].includes(this.request.method) ? this.bodyParams : this.queryParams; +}); diff --git a/app/api/server/index.js b/app/api/server/index.js new file mode 100644 index 000000000000..da65a35e8502 --- /dev/null +++ b/app/api/server/index.js @@ -0,0 +1,34 @@ +import './settings'; +import './helpers/composeRoomWithLastMessage'; +import './helpers/deprecationWarning'; +import './helpers/getLoggedInUser'; +import './helpers/getPaginationItems'; +import './helpers/getUserFromParams'; +import './helpers/getUserInfo'; +import './helpers/insertUserObject'; +import './helpers/isUserFromParams'; +import './helpers/parseJsonQuery'; +import './helpers/requestParams'; +import './default/info'; +import './v1/assets'; +import './v1/channels'; +import './v1/chat'; +import './v1/commands'; +import './v1/e2e'; +import './v1/emoji-custom'; +import './v1/groups'; +import './v1/im'; +import './v1/integrations'; +import './v1/import'; +import './v1/misc'; +import './v1/permissions'; +import './v1/push'; +import './v1/roles'; +import './v1/rooms'; +import './v1/settings'; +import './v1/stats'; +import './v1/subscriptions'; +import './v1/users'; +import './v1/video-conference'; + +export { API } from './api'; diff --git a/app/api/server/settings.js b/app/api/server/settings.js new file mode 100644 index 000000000000..afc90469617d --- /dev/null +++ b/app/api/server/settings.js @@ -0,0 +1,14 @@ +import { settings } from '../../settings'; + +settings.addGroup('General', function() { + this.section('REST API', function() { + this.add('API_Upper_Count_Limit', 100, { type: 'int', public: false }); + this.add('API_Default_Count', 50, { type: 'int', public: false }); + this.add('API_Allow_Infinite_Count', true, { type: 'boolean', public: false }); + this.add('API_Enable_Direct_Message_History_EndPoint', false, { type: 'boolean', public: false }); + this.add('API_Enable_Shields', true, { type: 'boolean', public: false }); + this.add('API_Shield_Types', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_Shields', value: true } }); + this.add('API_Enable_CORS', false, { type: 'boolean', public: false }); + this.add('API_CORS_Origin', '*', { type: 'string', public: false, enableQuery: { _id: 'API_Enable_CORS', value: true } }); + }); +}); diff --git a/app/api/server/v1/assets.js b/app/api/server/v1/assets.js new file mode 100644 index 000000000000..eacf92ae31cd --- /dev/null +++ b/app/api/server/v1/assets.js @@ -0,0 +1,57 @@ +import { Meteor } from 'meteor/meteor'; +import Busboy from 'busboy'; + +import { RocketChatAssets } from '../../../assets'; +import { API } from '../api'; + +API.v1.addRoute('assets.setAsset', { authRequired: true }, { + post() { + const busboy = new Busboy({ headers: this.request.headers }); + const fields = {}; + let asset = {}; + + Meteor.wrapAsync((callback) => { + busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); + busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { + const isValidAsset = Object.keys(RocketChatAssets.assets).includes(fieldname); + if (!isValidAsset) { + callback(new Meteor.Error('error-invalid-asset', 'Invalid asset')); + } + const assetData = []; + file.on('data', Meteor.bindEnvironment((data) => { + assetData.push(data); + })); + + file.on('end', Meteor.bindEnvironment(() => { + asset = { + buffer: Buffer.concat(assetData), + name: fieldname, + mimetype, + }; + })); + })); + busboy.on('finish', () => callback()); + this.request.pipe(busboy); + })(); + Meteor.runAsUser(this.userId, () => Meteor.call('setAsset', asset.buffer, asset.mimetype, asset.name)); + if (fields.refreshAllClients) { + Meteor.runAsUser(this.userId, () => Meteor.call('refreshClients')); + } + return API.v1.success(); + }, +}); + +API.v1.addRoute('assets.unsetAsset', { authRequired: true }, { + post() { + const { assetName, refreshAllClients } = this.bodyParams; + const isValidAsset = Object.keys(RocketChatAssets.assets).includes(assetName); + if (!isValidAsset) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); + } + Meteor.runAsUser(this.userId, () => Meteor.call('unsetAsset', assetName)); + if (refreshAllClients) { + Meteor.runAsUser(this.userId, () => Meteor.call('refreshClients')); + } + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js new file mode 100644 index 000000000000..1a4e74afbbcb --- /dev/null +++ b/app/api/server/v1/channels.js @@ -0,0 +1,1047 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models'; +import { hasPermission } from '../../../authorization'; +import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import { API } from '../api'; +import { settings } from '../../../settings'; + + +// Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property +function findChannelByIdOrName({ params, checkedArchived = true, userId }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { + throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); + } + + const fields = { ...API.v1.defaultFieldsToExclude }; + + let room; + if (params.roomId) { + room = Rooms.findOneById(params.roomId, { fields }); + } else if (params.roomName) { + room = Rooms.findOneByName(params.roomName, { fields }); + } + + 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'); + } + + if (checkedArchived && room.archived) { + throw new Meteor.Error('error-room-archived', `The channel, ${ room.name }, is archived`); + } + if (userId && room.lastMessage) { + const [lastMessage] = normalizeMessagesForUser([room.lastMessage], userId); + room.lastMessage = lastMessage; + } + + return room; +} + +API.v1.addRoute('channels.addAll', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addAllUserToRoom', findResult._id, this.bodyParams.activeUsersOnly); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.addModerator', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomModerator', findResult._id, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.addOwner', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomOwner', findResult._id, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.archive', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('archiveRoom', findResult._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.close', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + + const sub = Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); + + if (!sub) { + return API.v1.failure(`The user/callee is not in the channel "${ findResult.name }.`); + } + + if (!sub.open) { + return API.v1.failure(`The channel, ${ findResult.name }, is already closed to the sender`); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('hideRoom', findResult._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.counters', { authRequired: true }, { + get() { + const access = hasPermission(this.userId, 'view-room-administration'); + const { userId } = this.requestParams(); + let user = this.userId; + let unreads = null; + let userMentions = null; + let unreadsFrom = null; + let joined = false; + let msgs = null; + let latest = null; + let members = null; + + if (userId) { + if (!access) { + return API.v1.unauthorized(); + } + user = userId; + } + const room = findChannelByIdOrName({ + params: this.requestParams(), + returnUsernames: true, + }); + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user); + const lm = room.lm ? room.lm : room._updatedAt; + + if (typeof subscription !== 'undefined' && subscription.open) { + unreads = Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls, lm); + unreadsFrom = subscription.ls || subscription.ts; + userMentions = subscription.userMentions; + joined = true; + } + + if (access || joined) { + msgs = room.msgs; + latest = lm; + members = room.usersCount; + } + + return API.v1.success({ + joined, + members, + unreads, + unreadsFrom, + msgs, + latest, + userMentions, + }); + }, +}); + +// Channel -> create + +function createChannelValidator(params) { + if (!hasPermission(params.user.value, 'create-c')) { + throw new Error('unauthorized'); + } + + if (!params.name || !params.name.value) { + throw new Error(`Param "${ params.name.key }" is required`); + } + + if (params.members && params.members.value && !_.isArray(params.members.value)) { + throw new Error(`Param "${ params.members.key }" must be an array if provided`); + } + + if (params.customFields && params.customFields.value && !(typeof params.customFields.value === 'object')) { + throw new Error(`Param "${ params.customFields.key }" must be an object if provided`); + } +} + +function createChannel(userId, params) { + const readOnly = typeof params.readOnly !== 'undefined' ? params.readOnly : false; + const id = Meteor.runAsUser(userId, () => Meteor.call('createChannel', params.name, params.members ? params.members : [], readOnly, params.customFields)); + + return { + channel: findChannelByIdOrName({ params: { roomId: id.rid }, userId: this.userId }), + }; +} + +API.channels = {}; +API.channels.create = { + validate: createChannelValidator, + execute: createChannel, +}; + +API.v1.addRoute('channels.create', { authRequired: true }, { + post() { + const { userId, bodyParams } = this; + + let error; + + try { + API.channels.create.validate({ + user: { + value: userId, + }, + name: { + value: bodyParams.name, + key: 'name', + }, + members: { + value: bodyParams.members, + key: 'members', + }, + }); + } catch (e) { + if (e.message === 'unauthorized') { + error = API.v1.unauthorized(); + } else { + error = API.v1.failure(e.message); + } + } + + if (error) { + return error; + } + + return API.v1.success(API.channels.create.execute(userId, bodyParams)); + }, +}); + +API.v1.addRoute('channels.delete', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('eraseRoom', findResult._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.files', { authRequired: true }, { + get() { + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + const addUserObjectToEveryObject = (file) => { + if (file.userId) { + file = this.insertUserObject({ object: file, userId: file.userId }); + } + return file; + }; + + Meteor.runAsUser(this.userId, () => { + Meteor.call('canAccessRoom', findResult._id, this.userId); + }); + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult._id }); + + const files = Uploads.find(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + files: files.map(addUserObjectToEveryObject), + count: + files.length, + offset, + total: Uploads.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'manage-integrations')) { + return API.v1.unauthorized(); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + + let includeAllPublicChannels = true; + if (typeof this.queryParams.includeAllPublicChannels !== 'undefined') { + includeAllPublicChannels = this.queryParams.includeAllPublicChannels === 'true'; + } + + let ourQuery = { + channel: `#${ findResult.name }`, + }; + + if (includeAllPublicChannels) { + ourQuery.channel = { + $in: [ourQuery.channel, 'all_public_channels'], + }; + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + ourQuery = Object.assign({}, query, ourQuery); + + const integrations = Integrations.find(ourQuery, { + sort: sort || { _createdAt: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + integrations, + count: integrations.length, + offset, + total: Integrations.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('channels.history', { authRequired: true }, { + get() { + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + + let latestDate = new Date(); + if (this.queryParams.latest) { + latestDate = new Date(this.queryParams.latest); + } + + let oldestDate = undefined; + if (this.queryParams.oldest) { + oldestDate = new Date(this.queryParams.oldest); + } + + const inclusive = this.queryParams.inclusive || false; + + let count = 20; + if (this.queryParams.count) { + count = parseInt(this.queryParams.count); + } + + let offset = 0; + if (this.queryParams.offset) { + offset = parseInt(this.queryParams.offset); + } + + const unreads = this.queryParams.unreads || false; + + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('getChannelHistory', { + rid: findResult._id, + latest: latestDate, + oldest: oldestDate, + inclusive, + offset, + count, + unreads, + }); + }); + + if (!result) { + return API.v1.unauthorized(); + } + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('channels.info', { authRequired: true }, { + get() { + return API.v1.success({ + channel: findChannelByIdOrName({ + params: this.requestParams(), + checkedArchived: false, + userId: this.userId, + }), + }); + }, +}); + +API.v1.addRoute('channels.invite', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addUserToRoom', { rid: findResult._id, username: user.username }); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.join', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('joinRoom', findResult._id, this.bodyParams.joinCode); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.kick', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeUserFromRoom', { rid: findResult._id, username: user.username }); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.leave', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('leaveRoom', findResult._id); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.list', { authRequired: true }, { + get: { + // This is defined as such only to provide an example of how the routes can be defined :X + action() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + const hasPermissionToSeeAllPublicChannels = hasPermission(this.userId, 'view-c-room'); + + const ourQuery = { ...query, t: 'c' }; + + if (!hasPermissionToSeeAllPublicChannels) { + if (!hasPermission(this.userId, 'view-joined-room')) { + return API.v1.unauthorized(); + } + const roomIds = Subscriptions.findByUserIdAndType(this.userId, 'c', { fields: { rid: 1 } }).fetch().map((s) => s.rid); + ourQuery._id = { $in: roomIds }; + } + + const cursor = Rooms.find(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + const total = cursor.count(); + + const rooms = cursor.fetch(); + + return API.v1.success({ + channels: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + count: rooms.length, + offset, + total, + }); + }, + }, +}); + +API.v1.addRoute('channels.list.joined', { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); + + // TODO: CACHE: Add Breacking notice since we removed the query param + const cursor = Rooms.findBySubscriptionTypeAndUserId('c', this.userId, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + const totalCount = cursor.count(); + const rooms = cursor.fetch(); + + return API.v1.success({ + channels: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + offset, + count: rooms.length, + total: totalCount, + }); + }, +}); + +API.v1.addRoute('channels.members', { authRequired: true }, { + get() { + const findResult = findChannelByIdOrName({ + params: this.requestParams(), + checkedArchived: false, + }); + + if (findResult.broadcast && !hasPermission(this.userId, 'view-broadcast-member-list')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort = {} } = this.parseJsonQuery(); + + const subscriptions = Subscriptions.findByRoomId(findResult._id, { + fields: { 'u._id': 1 }, + sort: { 'u.username': sort.username != null ? sort.username : 1 }, + skip: offset, + limit: count, + }); + + const total = subscriptions.count(); + + const members = subscriptions.fetch().map((s) => s.u && s.u._id); + + const users = Users.find({ _id: { $in: members } }, { + fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, + sort: { username: sort.username != null ? sort.username : 1 }, + }).fetch(); + + return API.v1.success({ + members: users, + count: users.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute('channels.messages', { authRequired: true }, { + get() { + const findResult = findChannelByIdOrName({ + params: this.requestParams(), + checkedArchived: false, + }); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult._id }); + + // Special check for the permissions + if (hasPermission(this.userId, 'view-joined-room') && !Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId, { fields: { _id: 1 } })) { + return API.v1.unauthorized(); + } + if (!hasPermission(this.userId, 'view-c-room')) { + return API.v1.unauthorized(); + } + + const cursor = Messages.find(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }); + + const total = cursor.count(); + const messages = cursor.fetch(); + + return API.v1.success({ + messages: normalizeMessagesForUser(messages, this.userId), + count: messages.length, + offset, + total, + }); + }, +}); +// TODO: CACHE: I dont like this method( functionality and how we implemented ) its very expensive +// TODO check if this code is better or not +// RocketChat.API.v1.addRoute('channels.online', { authRequired: true }, { +// get() { +// const { query } = this.parseJsonQuery(); +// const ourQuery = Object.assign({}, query, { t: 'c' }); + +// const room = RocketChat.models.Rooms.findOne(ourQuery); + +// if (room == null) { +// return RocketChat.API.v1.failure('Channel does not exists'); +// } + +// const ids = RocketChat.models.Subscriptions.find({ rid: room._id }, { fields: { 'u._id': 1 } }).fetch().map(sub => sub.u._id); + +// const online = RocketChat.models.Users.find({ +// username: { $exists: 1 }, +// _id: { $in: ids }, +// status: { $in: ['online', 'away', 'busy'] } +// }, { +// fields: { username: 1 } +// }).fetch(); + +// return RocketChat.API.v1.success({ +// online +// }); +// } +// }); + +API.v1.addRoute('channels.online', { authRequired: true }, { + get() { + const { query } = this.parseJsonQuery(); + const ourQuery = Object.assign({}, query, { t: 'c' }); + + const room = Rooms.findOne(ourQuery); + + if (room == null) { + return API.v1.failure('Channel does not exists'); + } + + const online = Users.findUsersNotOffline({ + fields: { username: 1 }, + }).fetch(); + + const onlineInRoom = []; + online.forEach((user) => { + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } }); + if (subscription) { + onlineInRoom.push({ + _id: user._id, + username: user.username, + }); + } + }); + + return API.v1.success({ + online: onlineInRoom, + }); + }, +}); + +API.v1.addRoute('channels.open', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + + const sub = Subscriptions.findOneByRoomIdAndUserId(findResult._id, this.userId); + + if (!sub) { + return API.v1.failure(`The user/callee is not in the channel "${ findResult.name }".`); + } + + if (sub.open) { + return API.v1.failure(`The channel, ${ findResult.name }, is already open to the sender`); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('openRoom', findResult._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.removeModerator', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomModerator', findResult._id, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.removeOwner', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomOwner', findResult._id, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.rename', { authRequired: true }, { + post() { + if (!this.bodyParams.name || !this.bodyParams.name.trim()) { + return API.v1.failure('The bodyParam "name" is required'); + } + + const findResult = findChannelByIdOrName({ params: { roomId: this.bodyParams.roomId } }); + + if (findResult.name === this.bodyParams.name) { + return API.v1.failure('The channel name is the same as what it would be renamed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomName', this.bodyParams.name); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: { roomId: this.bodyParams.roomId }, userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.setCustomFields', { authRequired: true }, { + post() { + if (!this.bodyParams.customFields || !(typeof this.bodyParams.customFields === 'object')) { + return API.v1.failure('The bodyParam "customFields" is required with a type like object.'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomCustomFields', this.bodyParams.customFields); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.setDefault', { authRequired: true }, { + post() { + if (typeof this.bodyParams.default === 'undefined') { + return API.v1.failure('The bodyParam "default" is required', 'error-channels-setdefault-is-same'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + if (findResult.default === this.bodyParams.default) { + return API.v1.failure('The channel default setting is the same as what it would be changed to.', 'error-channels-setdefault-missing-default-param'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'default', this.bodyParams.default.toString()); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.setDescription', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('description')) { + return API.v1.failure('The bodyParam "description" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + if (findResult.description === this.bodyParams.description) { + return API.v1.failure('The channel description is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomDescription', this.bodyParams.description); + }); + + return API.v1.success({ + description: this.bodyParams.description, + }); + }, +}); + + +API.v1.addRoute('channels.setJoinCode', { authRequired: true }, { + post() { + if (!this.bodyParams.joinCode || !this.bodyParams.joinCode.trim()) { + return API.v1.failure('The bodyParam "joinCode" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'joinCode', this.bodyParams.joinCode); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.setPurpose', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('purpose')) { + return API.v1.failure('The bodyParam "purpose" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + if (findResult.description === this.bodyParams.purpose) { + return API.v1.failure('The channel purpose (description) is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomDescription', this.bodyParams.purpose); + }); + + return API.v1.success({ + purpose: this.bodyParams.purpose, + }); + }, +}); + +API.v1.addRoute('channels.setReadOnly', { authRequired: true }, { + post() { + if (typeof this.bodyParams.readOnly === 'undefined') { + return API.v1.failure('The bodyParam "readOnly" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + if (findResult.ro === this.bodyParams.readOnly) { + return API.v1.failure('The channel read only setting is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'readOnly', this.bodyParams.readOnly); + }); + + return API.v1.success({ + channel: findChannelByIdOrName({ params: this.requestParams(), userId: this.userId }), + }); + }, +}); + +API.v1.addRoute('channels.setTopic', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('topic')) { + return API.v1.failure('The bodyParam "topic" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + if (findResult.topic === this.bodyParams.topic) { + return API.v1.failure('The channel topic is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomTopic', this.bodyParams.topic); + }); + + return API.v1.success({ + topic: this.bodyParams.topic, + }); + }, +}); + +API.v1.addRoute('channels.setAnnouncement', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('announcement')) { + return API.v1.failure('The bodyParam "announcement" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomAnnouncement', this.bodyParams.announcement); + }); + + return API.v1.success({ + announcement: this.bodyParams.announcement, + }); + }, +}); + +API.v1.addRoute('channels.setType', { authRequired: true }, { + post() { + if (!this.bodyParams.type || !this.bodyParams.type.trim()) { + return API.v1.failure('The bodyParam "type" is required'); + } + + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + if (findResult.t === this.bodyParams.type) { + return API.v1.failure('The channel type is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult._id, 'roomType', this.bodyParams.type); + }); + + return API.v1.success({ + channel: this.composeRoomWithLastMessage(Rooms.findOneById(findResult._id, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('channels.unarchive', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams(), checkedArchived: false }); + + if (!findResult.archived) { + return API.v1.failure(`The channel, ${ findResult.name }, is not archived`); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('unarchiveRoom', findResult._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.getAllUserMentionsByChannel', { authRequired: true }, { + get() { + const { roomId } = this.requestParams(); + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + + if (!roomId) { + return API.v1.failure('The request param "roomId" is required'); + } + + const mentions = Meteor.runAsUser(this.userId, () => Meteor.call('getUserMentionsByChannel', { + roomId, + options: { + sort: sort || { ts: 1 }, + skip: offset, + limit: count, + }, + })); + + const allMentions = Meteor.runAsUser(this.userId, () => Meteor.call('getUserMentionsByChannel', { + roomId, + options: {}, + })); + + return API.v1.success({ + mentions, + count: mentions.length, + offset, + total: allMentions.length, + }); + }, +}); + +API.v1.addRoute('channels.roles', { authRequired: true }, { + get() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const roles = Meteor.runAsUser(this.userId, () => Meteor.call('getRoomRoles', findResult._id)); + + return API.v1.success({ + roles, + }); + }, +}); + +API.v1.addRoute('channels.moderators', { authRequired: true }, { + get() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const moderators = Subscriptions.findByRoomIdAndRoles(findResult._id, ['moderator'], { fields: { u: 1 } }).fetch().map((sub) => sub.u); + + return API.v1.success({ + moderators, + }); + }, +}); + +API.v1.addRoute('channels.addLeader', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomLeader', findResult._id, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.removeLeader', { authRequired: true }, { + post() { + const findResult = findChannelByIdOrName({ params: this.requestParams() }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomLeader', findResult._id, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('channels.anonymousread', { authRequired: false }, { + get() { + const findResult = findChannelByIdOrName({ + params: this.requestParams(), + checkedArchived: false, + }); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult._id }); + + if (!settings.get('Accounts_AllowAnonymousRead')) { + throw new Meteor.Error('error-not-allowed', 'Enable "Allow Anonymous Read"', { + method: 'channels.anonymousread', + }); + } + + const cursor = Messages.find(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }); + + const total = cursor.count(); + const messages = cursor.fetch(); + + return API.v1.success({ + messages: normalizeMessagesForUser(messages, this.userId), + count: messages.length, + offset, + total, + }); + }, +}); diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js new file mode 100644 index 000000000000..733b9614da51 --- /dev/null +++ b/app/api/server/v1/chat.js @@ -0,0 +1,570 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; + +import { Messages } from '../../../models'; +import { canAccessRoom, hasPermission } from '../../../authorization'; +import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import { processWebhookMessage } from '../../../lib'; +import { API } from '../api'; +import Rooms from '../../../models/server/models/Rooms'; +import Users from '../../../models/server/models/Users'; +import { settings } from '../../../settings'; + +API.v1.addRoute('chat.delete', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + msgId: String, + roomId: String, + asUser: Match.Maybe(Boolean), + })); + + const msg = Messages.findOneById(this.bodyParams.msgId, { fields: { u: 1, rid: 1 } }); + + if (!msg) { + return API.v1.failure(`No message found with the id of "${ this.bodyParams.msgId }".`); + } + + if (this.bodyParams.roomId !== msg.rid) { + return API.v1.failure('The room id provided does not match where the message is from.'); + } + + if (this.bodyParams.asUser && msg.u._id !== this.userId && !hasPermission(this.userId, 'force-delete-message', msg.rid)) { + return API.v1.failure('Unauthorized. You must have the permission "force-delete-message" to delete other\'s message as them.'); + } + + Meteor.runAsUser(this.bodyParams.asUser ? msg.u._id : this.userId, () => { + Meteor.call('deleteMessage', { _id: msg._id }); + }); + + return API.v1.success({ + _id: msg._id, + ts: Date.now(), + message: msg, + }); + }, +}); + +API.v1.addRoute('chat.syncMessages', { authRequired: true }, { + get() { + const { roomId, lastUpdate } = this.queryParams; + + if (!roomId) { + throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); + } + + if (!lastUpdate) { + throw new Meteor.Error('error-lastUpdate-param-not-provided', 'The required "lastUpdate" query param is missing.'); + } else if (isNaN(Date.parse(lastUpdate))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); + } + + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('messages/get', roomId, { lastUpdate: new Date(lastUpdate) }); + }); + + if (!result) { + return API.v1.failure(); + } + + return API.v1.success({ + result: { + updated: normalizeMessagesForUser(result.updated, this.userId), + deleted: normalizeMessagesForUser(result.deleted, this.userId), + }, + }); + }, +}); + +API.v1.addRoute('chat.getMessage', { authRequired: true }, { + get() { + if (!this.queryParams.msgId) { + return API.v1.failure('The "msgId" query parameter must be provided.'); + } + + let msg; + Meteor.runAsUser(this.userId, () => { + msg = Meteor.call('getSingleMessage', this.queryParams.msgId); + }); + + if (!msg) { + return API.v1.failure(); + } + + const [message] = normalizeMessagesForUser([msg], this.userId); + + return API.v1.success({ + message, + }); + }, +}); + +API.v1.addRoute('chat.pinMessage', { authRequired: true }, { + post() { + if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); + } + + const msg = Messages.findOneById(this.bodyParams.messageId); + + if (!msg) { + throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); + } + + let pinnedMessage; + Meteor.runAsUser(this.userId, () => { pinnedMessage = Meteor.call('pinMessage', msg); }); + + const [message] = normalizeMessagesForUser([pinnedMessage], this.userId); + + return API.v1.success({ + message, + }); + }, +}); + +API.v1.addRoute('chat.postMessage', { authRequired: true }, { + post() { + const messageReturn = processWebhookMessage(this.bodyParams, this.user, undefined, true)[0]; + + if (!messageReturn) { + return API.v1.failure('unknown-error'); + } + + const [message] = normalizeMessagesForUser([messageReturn.message], this.userId); + + return API.v1.success({ + ts: Date.now(), + channel: messageReturn.channel, + message, + }); + }, +}); + +API.v1.addRoute('chat.search', { authRequired: true }, { + get() { + const { roomId, searchText } = this.queryParams; + const { count } = this.getPaginationItems(); + + if (!roomId) { + throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); + } + + if (!searchText) { + throw new Meteor.Error('error-searchText-param-not-provided', 'The required "searchText" query param is missing.'); + } + + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('messageSearch', searchText, roomId, count).message.docs; }); + + return API.v1.success({ + messages: normalizeMessagesForUser(result, this.userId), + }); + }, +}); + +// The difference between `chat.postMessage` and `chat.sendMessage` is that `chat.sendMessage` allows +// for passing a value for `_id` and the other one doesn't. Also, `chat.sendMessage` only sends it to +// one channel whereas the other one allows for sending to more than one channel at a time. +API.v1.addRoute('chat.sendMessage', { authRequired: true }, { + post() { + if (!this.bodyParams.message) { + throw new Meteor.Error('error-invalid-params', 'The "message" parameter must be provided.'); + } + + const sent = Meteor.runAsUser(this.userId, () => Meteor.call('sendMessage', this.bodyParams.message)); + + const [message] = normalizeMessagesForUser([sent], this.userId); + + return API.v1.success({ + message, + }); + }, +}); + +API.v1.addRoute('chat.starMessage', { authRequired: true }, { + post() { + if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); + } + + const msg = Messages.findOneById(this.bodyParams.messageId); + + if (!msg) { + throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('starMessage', { + _id: msg._id, + rid: msg.rid, + starred: true, + })); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.unPinMessage', { authRequired: true }, { + post() { + if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); + } + + const msg = Messages.findOneById(this.bodyParams.messageId); + + if (!msg) { + throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('unpinMessage', msg)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.unStarMessage', { authRequired: true }, { + post() { + if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is required.'); + } + + const msg = Messages.findOneById(this.bodyParams.messageId); + + if (!msg) { + throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('starMessage', { + _id: msg._id, + rid: msg.rid, + starred: false, + })); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.update', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + roomId: String, + msgId: String, + text: String, // Using text to be consistant with chat.postMessage + })); + + const msg = Messages.findOneById(this.bodyParams.msgId); + + // Ensure the message exists + if (!msg) { + return API.v1.failure(`No message found with the id of "${ this.bodyParams.msgId }".`); + } + + if (this.bodyParams.roomId !== msg.rid) { + return API.v1.failure('The room id provided does not match where the message is from.'); + } + + // Permission checks are already done in the updateMessage method, so no need to duplicate them + Meteor.runAsUser(this.userId, () => { + Meteor.call('updateMessage', { _id: msg._id, msg: this.bodyParams.text, rid: msg.rid }); + }); + + const [message] = normalizeMessagesForUser([Messages.findOneById(msg._id)], this.userId); + + return API.v1.success({ + message, + }); + }, +}); + +API.v1.addRoute('chat.react', { authRequired: true }, { + post() { + if (!this.bodyParams.messageId || !this.bodyParams.messageId.trim()) { + throw new Meteor.Error('error-messageid-param-not-provided', 'The required "messageId" param is missing.'); + } + + const msg = Messages.findOneById(this.bodyParams.messageId); + + if (!msg) { + throw new Meteor.Error('error-message-not-found', 'The provided "messageId" does not match any existing message.'); + } + + const emoji = this.bodyParams.emoji || this.bodyParams.reaction; + + if (!emoji) { + throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('setReaction', emoji, msg._id, this.bodyParams.shouldReact)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.getMessageReadReceipts', { authRequired: true }, { + get() { + const { messageId } = this.queryParams; + if (!messageId) { + return API.v1.failure({ + error: 'The required \'messageId\' param is missing.', + }); + } + + try { + const messageReadReceipts = Meteor.runAsUser(this.userId, () => Meteor.call('getReadReceipts', { messageId })); + return API.v1.success({ + receipts: messageReadReceipts, + }); + } catch (error) { + return API.v1.failure({ + error: error.message, + }); + } + }, +}); + +API.v1.addRoute('chat.reportMessage', { authRequired: true }, { + post() { + const { messageId, description } = this.bodyParams; + if (!messageId) { + return API.v1.failure('The required "messageId" param is missing.'); + } + + if (!description) { + return API.v1.failure('The required "description" param is missing.'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('reportMessage', messageId, description)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.ignoreUser', { authRequired: true }, { + get() { + const { rid, userId } = this.queryParams; + let { ignore = true } = this.queryParams; + + ignore = typeof ignore === 'string' ? /true|1/.test(ignore) : ignore; + + if (!rid || !rid.trim()) { + throw new Meteor.Error('error-room-id-param-not-provided', 'The required "rid" param is missing.'); + } + + if (!userId || !userId.trim()) { + throw new Meteor.Error('error-user-id-param-not-provided', 'The required "userId" param is missing.'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('ignoreUser', { rid, userId, ignore })); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.getDeletedMessages', { authRequired: true }, { + get() { + const { roomId, since } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + + if (!roomId) { + throw new Meteor.Error('The required "roomId" query param is missing.'); + } + + if (!since) { + throw new Meteor.Error('The required "since" query param is missing.'); + } else if (isNaN(Date.parse(since))) { + throw new Meteor.Error('The "since" query parameter must be a valid date.'); + } + const cursor = Messages.trashFindDeletedAfter(new Date(since), { rid: roomId }, { + skip: offset, + limit: count, + fields: { _id: 1 }, + }); + + const total = cursor.count(); + + const messages = cursor.fetch(); + + return API.v1.success({ + messages, + count: messages.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute('chat.getThreadsList', { authRequired: true }, { + get() { + const { rid } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + if (!rid) { + throw new Meteor.Error('The required "rid" query param is missing.'); + } + if (!settings.get('Threads_enabled')) { + throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); + } + const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); + const room = Rooms.findOneById(rid, { fields: { t: 1, _id: 1 } }); + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not Allowed'); + } + const threadQuery = Object.assign({}, query, { rid, tcount: { $exists: true } }); + const cursor = Messages.find(threadQuery, { + sort: sort || { ts: 1 }, + skip: offset, + limit: count, + fields, + }); + + const total = cursor.count(); + + const threads = cursor.fetch(); + + return API.v1.success({ + threads, + count: threads.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute('chat.syncThreadsList', { authRequired: true }, { + get() { + const { rid } = this.queryParams; + const { query, fields, sort } = this.parseJsonQuery(); + const { updatedSince } = this.queryParams; + let updatedSinceDate; + if (!settings.get('Threads_enabled')) { + throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); + } + if (!rid) { + throw new Meteor.Error('error-room-id-param-not-provided', 'The required "rid" query param is missing.'); + } + if (!updatedSince) { + throw new Meteor.Error('error-updatedSince-param-invalid', 'The required param "updatedSince" is missing.'); + } + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); + const room = Rooms.findOneById(rid, { fields: { t: 1, _id: 1 } }); + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not Allowed'); + } + const threadQuery = Object.assign({}, query, { rid, tcount: { $exists: true } }); + return API.v1.success({ + threads: { + update: Messages.find({ ...threadQuery, _updatedAt: { $gt: updatedSinceDate } }, { fields, sort }).fetch(), + remove: Messages.trashFindDeletedAfter(updatedSinceDate, threadQuery, { fields, sort }).fetch(), + }, + }); + }, +}); + +API.v1.addRoute('chat.getThreadMessages', { authRequired: true }, { + get() { + const { tmid } = this.queryParams; + const { query, fields, sort } = this.parseJsonQuery(); + const { offset, count } = this.getPaginationItems(); + + if (!settings.get('Threads_enabled')) { + throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); + } + if (!tmid) { + throw new Meteor.Error('error-invalid-params', 'The required "tmid" query param is missing.'); + } + const thread = Messages.findOneById(tmid, { fields: { rid: 1 } }); + if (!thread || !thread.rid) { + throw new Meteor.Error('error-invalid-message', 'Invalid Message'); + } + const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); + const room = Rooms.findOneById(thread.rid, { fields: { t: 1, _id: 1 } }); + + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not Allowed'); + } + const cursor = Messages.find({ ...query, tmid }, { + sort: sort || { ts: 1 }, + skip: offset, + limit: count, + fields, + }); + + const total = cursor.count(); + + const messages = cursor.fetch(); + + return API.v1.success({ + messages, + count: messages.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute('chat.syncThreadMessages', { authRequired: true }, { + get() { + const { tmid } = this.queryParams; + const { query, fields, sort } = this.parseJsonQuery(); + const { updatedSince } = this.queryParams; + let updatedSinceDate; + if (!settings.get('Threads_enabled')) { + throw new Meteor.Error('error-not-allowed', 'Threads Disabled'); + } + if (!tmid) { + throw new Meteor.Error('error-invalid-params', 'The required "tmid" query param is missing.'); + } + if (!updatedSince) { + throw new Meteor.Error('error-updatedSince-param-invalid', 'The required param "updatedSince" is missing.'); + } + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + const thread = Messages.findOneById(tmid, { fields: { rid: 1 } }); + if (!thread || !thread.rid) { + throw new Meteor.Error('error-invalid-message', 'Invalid Message'); + } + const user = Users.findOneById(this.userId, { fields: { _id: 1 } }); + const room = Rooms.findOneById(thread.rid, { fields: { t: 1, _id: 1 } }); + + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not Allowed'); + } + return API.v1.success({ + messages: { + update: Messages.find({ ...query, tmid, _updatedAt: { $gt: updatedSinceDate } }, { fields, sort }).fetch(), + remove: Messages.trashFindDeletedAfter(updatedSinceDate, { ...query, tmid }, { fields, sort }).fetch(), + }, + }); + }, +}); + +API.v1.addRoute('chat.followMessage', { authRequired: true }, { + post() { + const { mid } = this.bodyParams; + + if (!mid) { + throw new Meteor.Error('The required "mid" body param is missing.'); + } + Meteor.runAsUser(this.userId, () => Meteor.call('followMessage', { mid })); + return API.v1.success(); + }, +}); + +API.v1.addRoute('chat.unfollowMessage', { authRequired: true }, { + post() { + const { mid } = this.bodyParams; + + if (!mid) { + throw new Meteor.Error('The required "mid" body param is missing.'); + } + Meteor.runAsUser(this.userId, () => Meteor.call('unfollowMessage', { mid })); + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/commands.js b/app/api/server/v1/commands.js new file mode 100644 index 000000000000..80e75fa2b6f9 --- /dev/null +++ b/app/api/server/v1/commands.js @@ -0,0 +1,171 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; + +import { slashCommands } from '../../../utils'; +import { Rooms } from '../../../models'; +import { API } from '../api'; + +API.v1.addRoute('commands.get', { authRequired: true }, { + get() { + const params = this.queryParams; + + if (typeof params.command !== 'string') { + return API.v1.failure('The query param "command" must be provided.'); + } + + const cmd = slashCommands.commands[params.command.toLowerCase()]; + + if (!cmd) { + return API.v1.failure(`There is no command in the system by the name of: ${ params.command }`); + } + + return API.v1.success({ command: cmd }); + }, +}); + +API.v1.addRoute('commands.list', { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + let commands = Object.values(slashCommands.commands); + + if (query && query.command) { + commands = commands.filter((command) => command.command === query.command); + } + + const totalCount = commands.length; + commands = Rooms.processQueryOptionsOnResult(commands, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + return API.v1.success({ + commands, + offset, + count: commands.length, + total: totalCount, + }); + }, +}); + +// Expects a body of: { command: 'gimme', params: 'any string value', roomId: 'value' } +API.v1.addRoute('commands.run', { authRequired: true }, { + post() { + const body = this.bodyParams; + const user = this.getLoggedInUser(); + + if (typeof body.command !== 'string') { + return API.v1.failure('You must provide a command to run.'); + } + + if (body.params && typeof body.params !== 'string') { + return API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof body.roomId !== 'string') { + return API.v1.failure('The room\'s id where to execute this command must be provided and be a string.'); + } + + const cmd = body.command.toLowerCase(); + if (!slashCommands.commands[body.command.toLowerCase()]) { + return API.v1.failure('The command provided does not exist (or is disabled).'); + } + + // This will throw an error if they can't or the room is invalid + Meteor.call('canAccessRoom', body.roomId, user._id); + + const params = body.params ? body.params : ''; + + let result; + Meteor.runAsUser(user._id, () => { + result = slashCommands.run(cmd, params, { + _id: Random.id(), + rid: body.roomId, + msg: `/${ cmd } ${ params }`, + }); + }); + + return API.v1.success({ result }); + }, +}); + +API.v1.addRoute('commands.preview', { authRequired: true }, { + // Expects these query params: command: 'giphy', params: 'mine', roomId: 'value' + get() { + const query = this.queryParams; + const user = this.getLoggedInUser(); + + if (typeof query.command !== 'string') { + return API.v1.failure('You must provide a command to get the previews from.'); + } + + if (query.params && typeof query.params !== 'string') { + return API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof query.roomId !== 'string') { + return API.v1.failure('The room\'s id where the previews are being displayed must be provided and be a string.'); + } + + const cmd = query.command.toLowerCase(); + if (!slashCommands.commands[cmd]) { + return API.v1.failure('The command provided does not exist (or is disabled).'); + } + + // This will throw an error if they can't or the room is invalid + Meteor.call('canAccessRoom', query.roomId, user._id); + + const params = query.params ? query.params : ''; + + let preview; + Meteor.runAsUser(user._id, () => { + preview = Meteor.call('getSlashCommandPreviews', { cmd, params, msg: { rid: query.roomId } }); + }); + + return API.v1.success({ preview }); + }, + // Expects a body format of: { command: 'giphy', params: 'mine', roomId: 'value', previewItem: { id: 'sadf8' type: 'image', value: 'https://dev.null/gif } } + post() { + const body = this.bodyParams; + const user = this.getLoggedInUser(); + + if (typeof body.command !== 'string') { + return API.v1.failure('You must provide a command to run the preview item on.'); + } + + if (body.params && typeof body.params !== 'string') { + return API.v1.failure('The parameters for the command must be a single string.'); + } + + if (typeof body.roomId !== 'string') { + return API.v1.failure('The room\'s id where the preview is being executed in must be provided and be a string.'); + } + + if (typeof body.previewItem === 'undefined') { + return API.v1.failure('The preview item being executed must be provided.'); + } + + if (!body.previewItem.id || !body.previewItem.type || typeof body.previewItem.value === 'undefined') { + return API.v1.failure('The preview item being executed is in the wrong format.'); + } + + const cmd = body.command.toLowerCase(); + if (!slashCommands.commands[cmd]) { + return API.v1.failure('The command provided does not exist (or is disabled).'); + } + + // This will throw an error if they can't or the room is invalid + Meteor.call('canAccessRoom', body.roomId, user._id); + + const params = body.params ? body.params : ''; + + Meteor.runAsUser(user._id, () => { + Meteor.call('executeSlashCommandPreview', { cmd, params, msg: { rid: body.roomId } }, body.previewItem); + }); + + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/e2e.js b/app/api/server/v1/e2e.js new file mode 100644 index 000000000000..f7a6f49108a6 --- /dev/null +++ b/app/api/server/v1/e2e.js @@ -0,0 +1,62 @@ +import { Meteor } from 'meteor/meteor'; + +import { API } from '../api'; + +API.v1.addRoute('e2e.fetchMyKeys', { authRequired: true }, { + get() { + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('e2e.fetchMyKeys'); }); + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('e2e.getUsersOfRoomWithoutKey', { authRequired: true }, { + get() { + const { rid } = this.queryParams; + + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('e2e.getUsersOfRoomWithoutKey', rid); }); + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('e2e.setRoomKeyID', { authRequired: true }, { + post() { + const { rid, keyID } = this.bodyParams; + + Meteor.runAsUser(this.userId, () => { + API.v1.success(Meteor.call('e2e.setRoomKeyID', rid, keyID)); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('e2e.setUserPublicAndPivateKeys', { authRequired: true }, { + post() { + const { public_key, private_key } = this.bodyParams; + + Meteor.runAsUser(this.userId, () => { + API.v1.success(Meteor.call('e2e.setUserPublicAndPivateKeys', { + public_key, + private_key, + })); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('e2e.updateGroupKey', { authRequired: true }, { + post() { + const { uid, rid, key } = this.bodyParams; + + Meteor.runAsUser(this.userId, () => { + API.v1.success(Meteor.call('e2e.updateGroupKey', rid, uid, key)); + }); + + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/emoji-custom.js b/app/api/server/v1/emoji-custom.js new file mode 100644 index 000000000000..243386792058 --- /dev/null +++ b/app/api/server/v1/emoji-custom.js @@ -0,0 +1,159 @@ +import { Meteor } from 'meteor/meteor'; +import Busboy from 'busboy'; + +import { EmojiCustom } from '../../../models'; +import { API } from '../api'; + +// DEPRECATED +// Will be removed after v1.12.0 +API.v1.addRoute('emoji-custom', { authRequired: true }, { + get() { + const warningMessage = 'The endpoint "emoji-custom" is deprecated and will be removed after version v1.12.0'; + console.warn(warningMessage); + const { query } = this.parseJsonQuery(); + const emojis = Meteor.call('listEmojiCustom', query); + + return API.v1.success(this.deprecationWarning({ + endpoint: 'emoji-custom', + versionWillBeRemoved: '1.12.0', + response: { + emojis, + }, + })); + }, +}); + +API.v1.addRoute('emoji-custom.list', { authRequired: true }, { + get() { + const { query } = this.parseJsonQuery(); + const { updatedSince } = this.queryParams; + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + return API.v1.success({ + emojis: { + update: EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).fetch(), + remove: EmojiCustom.trashFindDeletedAfter(updatedSinceDate).fetch(), + }, + }); + } + + return API.v1.success({ + emojis: { + update: EmojiCustom.find(query).fetch(), + remove: [], + }, + }); + }, +}); + +API.v1.addRoute('emoji-custom.create', { authRequired: true }, { + post() { + Meteor.runAsUser(this.userId, () => { + const fields = {}; + const busboy = new Busboy({ headers: this.request.headers }); + const emojiData = []; + let emojiMimetype = ''; + + Meteor.wrapAsync((callback) => { + busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'emoji') { + return callback(new Meteor.Error('invalid-field')); + } + + file.on('data', Meteor.bindEnvironment((data) => emojiData.push(data))); + + file.on('end', Meteor.bindEnvironment(() => { + const extension = mimetype.split('/')[1]; + emojiMimetype = mimetype; + fields.extension = extension; + })); + })); + busboy.on('field', (fieldname, val) => { + fields[fieldname] = val; + }); + busboy.on('finish', Meteor.bindEnvironment(() => { + fields.newFile = true; + fields.aliases = fields.aliases || ''; + try { + Meteor.call('insertOrUpdateEmoji', fields); + Meteor.call('uploadEmojiCustom', Buffer.concat(emojiData), emojiMimetype, fields); + callback(); + } catch (error) { + return callback(error); + } + })); + this.request.pipe(busboy); + })(); + }); + }, +}); + +API.v1.addRoute('emoji-custom.update', { authRequired: true }, { + post() { + Meteor.runAsUser(this.userId, () => { + const fields = {}; + const busboy = new Busboy({ headers: this.request.headers }); + const emojiData = []; + let emojiMimetype = ''; + + Meteor.wrapAsync((callback) => { + busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'emoji') { + return callback(new Meteor.Error('invalid-field')); + } + file.on('data', Meteor.bindEnvironment((data) => emojiData.push(data))); + + file.on('end', Meteor.bindEnvironment(() => { + const extension = mimetype.split('/')[1]; + emojiMimetype = mimetype; + fields.extension = extension; + })); + })); + busboy.on('field', (fieldname, val) => { + fields[fieldname] = val; + }); + busboy.on('finish', Meteor.bindEnvironment(() => { + try { + if (!fields._id) { + return callback(new Meteor.Error('The required "_id" query param is missing.')); + } + const emojiToUpdate = EmojiCustom.findOneById(fields._id); + if (!emojiToUpdate) { + return callback(new Meteor.Error('Emoji not found.')); + } + fields.previousName = emojiToUpdate.name; + fields.previousExtension = emojiToUpdate.extension; + fields.aliases = fields.aliases || ''; + fields.newFile = Boolean(emojiData.length); + Meteor.call('insertOrUpdateEmoji', fields); + if (emojiData.length) { + Meteor.call('uploadEmojiCustom', Buffer.concat(emojiData), emojiMimetype, fields); + } + callback(); + } catch (error) { + return callback(error); + } + })); + this.request.pipe(busboy); + })(); + }); + }, +}); + +API.v1.addRoute('emoji-custom.delete', { authRequired: true }, { + post() { + const { emojiId } = this.bodyParams; + if (!emojiId) { + return API.v1.failure('The "emojiId" params is required!'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('deleteEmojiCustom', emojiId)); + + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js new file mode 100644 index 000000000000..b84c1d171a06 --- /dev/null +++ b/app/api/server/v1/groups.js @@ -0,0 +1,815 @@ +import _ from 'underscore'; +import { Meteor } from 'meteor/meteor'; + +import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server'; +import { hasPermission, canAccessRoom } from '../../../authorization/server'; +import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import { API } from '../api'; + +// Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property +function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { + throw new Meteor.Error('error-room-param-not-provided', 'The parameter "roomId" or "roomName" is required'); + } + + const roomOptions = { + fields: { + t: 1, + ro: 1, + name: 1, + fname: 1, + prid: 1, + archived: 1, + }, + }; + const room = params.roomId + ? Rooms.findOneById(params.roomId, roomOptions) + : Rooms.findOneByName(params.roomName, roomOptions); + + if (!room || room.t !== 'p') { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } + + const user = Users.findOneById(userId, { fields: { username: 1 } }); + + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } + + // discussions have their names saved on `fname` property + const roomName = room.prid ? room.fname : room.name; + + if (checkedArchived && room.archived) { + throw new Meteor.Error('error-room-archived', `The private group, ${ roomName }, is archived`); + } + + const sub = Subscriptions.findOneByRoomIdAndUserId(room._id, userId, { fields: { open: 1 } }); + + return { + rid: room._id, + open: sub && sub.open, + ro: room.ro, + t: room.t, + name: roomName, + }; +} + +API.v1.addRoute('groups.addAll', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addAllUserToRoom', findResult.rid, this.bodyParams.activeUsersOnly); + }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(findResult.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.addModerator', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomModerator', findResult.rid, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.addOwner', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomOwner', findResult.rid, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.addLeader', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + const user = this.getUserFromParams(); + Meteor.runAsUser(this.userId, () => { + Meteor.call('addRoomLeader', findResult.rid, user._id); + }); + + return API.v1.success(); + }, +}); + +// Archives a private group only if it wasn't +API.v1.addRoute('groups.archive', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('archiveRoom', findResult.rid); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.close', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + if (!findResult.open) { + return API.v1.failure(`The private group, ${ findResult.name }, is already closed to the sender`); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('hideRoom', findResult.rid); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.counters', { authRequired: true }, { + get() { + const access = hasPermission(this.userId, 'view-room-administration'); + const params = this.requestParams(); + let user = this.userId; + let room; + let unreads = null; + let userMentions = null; + let unreadsFrom = null; + let joined = false; + let msgs = null; + let latest = null; + let members = null; + + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { + throw new Meteor.Error('error-room-param-not-provided', 'The parameter "roomId" or "roomName" is required'); + } + + if (params.roomId) { + room = Rooms.findOneById(params.roomId); + } else if (params.roomName) { + room = Rooms.findOneByName(params.roomName); + } + + if (!room || room.t !== 'p') { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } + + if (room.archived) { + throw new Meteor.Error('error-room-archived', `The private group, ${ room.name }, is archived`); + } + + if (params.userId) { + if (!access) { + return API.v1.unauthorized(); + } + user = params.userId; + } + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user); + const lm = room.lm ? room.lm : room._updatedAt; + + if (typeof subscription !== 'undefined' && subscription.open) { + unreads = Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls || subscription.ts, lm); + unreadsFrom = subscription.ls || subscription.ts; + userMentions = subscription.userMentions; + joined = true; + } + + if (access || joined) { + msgs = room.msgs; + latest = lm; + members = room.usersCount; + } + + return API.v1.success({ + joined, + members, + unreads, + unreadsFrom, + msgs, + latest, + userMentions, + }); + }, +}); + +// Create Private Group +API.v1.addRoute('groups.create', { authRequired: true }, { + post() { + if (!hasPermission(this.userId, 'create-p')) { + return API.v1.unauthorized(); + } + + if (!this.bodyParams.name) { + return API.v1.failure('Body param "name" is required'); + } + + if (this.bodyParams.members && !_.isArray(this.bodyParams.members)) { + return API.v1.failure('Body param "members" must be an array if provided'); + } + + if (this.bodyParams.customFields && !(typeof this.bodyParams.customFields === 'object')) { + return API.v1.failure('Body param "customFields" must be an object if provided'); + } + + const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false; + + let id; + Meteor.runAsUser(this.userId, () => { + id = Meteor.call('createPrivateGroup', this.bodyParams.name, this.bodyParams.members ? this.bodyParams.members : [], readOnly, this.bodyParams.customFields); + }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(id.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.delete', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('eraseRoom', findResult.rid); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.files', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + const addUserObjectToEveryObject = (file) => { + if (file.userId) { + file = this.insertUserObject({ object: file, userId: file.userId }); + } + return file; + }; + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult.rid }); + + const files = Uploads.find(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + files: files.map(addUserObjectToEveryObject), + count: files.length, + offset, + total: Uploads.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'manage-integrations')) { + return API.v1.unauthorized(); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + let includeAllPrivateGroups = true; + if (typeof this.queryParams.includeAllPrivateGroups !== 'undefined') { + includeAllPrivateGroups = this.queryParams.includeAllPrivateGroups === 'true'; + } + + const channelsToSearch = [`#${ findResult.name }`]; + if (includeAllPrivateGroups) { + channelsToSearch.push('all_private_groups'); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { channel: { $in: channelsToSearch } }); + const integrations = Integrations.find(ourQuery, { + sort: sort || { _createdAt: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + integrations, + count: integrations.length, + offset, + total: Integrations.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('groups.history', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + let latestDate = new Date(); + if (this.queryParams.latest) { + latestDate = new Date(this.queryParams.latest); + } + + let oldestDate = undefined; + if (this.queryParams.oldest) { + oldestDate = new Date(this.queryParams.oldest); + } + + const inclusive = this.queryParams.inclusive || false; + + let count = 20; + if (this.queryParams.count) { + count = parseInt(this.queryParams.count); + } + + let offset = 0; + if (this.queryParams.offset) { + offset = parseInt(this.queryParams.offset); + } + + const unreads = this.queryParams.unreads || false; + + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('getChannelHistory', { rid: findResult.rid, latest: latestDate, oldest: oldestDate, inclusive, offset, count, unreads }); + }); + + if (!result) { + return API.v1.unauthorized(); + } + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('groups.info', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(findResult.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.invite', { authRequired: true }, { + post() { + const { roomId = '', roomName = '' } = this.requestParams(); + const idOrName = roomId || roomName; + if (!idOrName.trim()) { + throw new Meteor.Error('error-room-param-not-provided', 'The parameter "roomId" or "roomName" is required'); + } + + const { _id: rid, t: type } = Rooms.findOneByIdOrName(idOrName) || {}; + + if (!rid || type !== 'p') { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group'); + } + + const { username } = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => Meteor.call('addUserToRoom', { rid, username })); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.kick', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeUserFromRoom', { rid: findResult.rid, username: user.username }); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.leave', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('leaveRoom', findResult.rid); + }); + + return API.v1.success(); + }, +}); + +// List Private Groups a user has access to +API.v1.addRoute('groups.list', { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields } = this.parseJsonQuery(); + + // TODO: CACHE: Add Breacking notice since we removed the query param + const cursor = Rooms.findBySubscriptionTypeAndUserId('p', this.userId, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + const totalCount = cursor.count(); + const rooms = cursor.fetch(); + + + return API.v1.success({ + groups: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + offset, + count: rooms.length, + total: totalCount, + }); + }, +}); + + +API.v1.addRoute('groups.listAll', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'view-room-administration')) { + return API.v1.unauthorized(); + } + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + const ourQuery = Object.assign({}, query, { t: 'p' }); + + let rooms = Rooms.find(ourQuery).fetch(); + const totalCount = rooms.length; + + rooms = Rooms.processQueryOptionsOnResult(rooms, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }); + + return API.v1.success({ + groups: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + offset, + count: rooms.length, + total: totalCount, + }); + }, +}); + +API.v1.addRoute('groups.members', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + const room = Rooms.findOneById(findResult.rid, { fields: { broadcast: 1 } }); + + if (room.broadcast && !hasPermission(this.userId, 'view-broadcast-member-list')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort = {} } = this.parseJsonQuery(); + + const subscriptions = Subscriptions.findByRoomId(findResult.rid, { + fields: { 'u._id': 1 }, + sort: { 'u.username': sort.username != null ? sort.username : 1 }, + skip: offset, + limit: count, + }); + + const total = subscriptions.count(); + + const members = subscriptions.fetch().map((s) => s.u && s.u._id); + + const users = Users.find({ _id: { $in: members } }, { + fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, + sort: { username: sort.username != null ? sort.username : 1 }, + }).fetch(); + + return API.v1.success({ + members: users, + count: users.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute('groups.messages', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult.rid }); + + const messages = Messages.find(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + messages: normalizeMessagesForUser(messages, this.userId), + count: messages.length, + offset, + total: Messages.find(ourQuery).count(), + }); + }, +}); +// TODO: CACHE: same as channels.online +API.v1.addRoute('groups.online', { authRequired: true }, { + get() { + const { query } = this.parseJsonQuery(); + const ourQuery = Object.assign({}, query, { t: 'p' }); + + const room = Rooms.findOne(ourQuery); + + if (room == null) { + return API.v1.failure('Group does not exists'); + } + + const online = Users.findUsersNotOffline({ + fields: { + username: 1, + }, + }).fetch(); + + const onlineInRoom = []; + online.forEach((user) => { + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } }); + if (subscription) { + onlineInRoom.push({ + _id: user._id, + username: user.username, + }); + } + }); + + return API.v1.success({ + online: onlineInRoom, + }); + }, +}); + +API.v1.addRoute('groups.open', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + if (findResult.open) { + return API.v1.failure(`The private group, ${ findResult.name }, is already open for the sender`); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('openRoom', findResult.rid); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.removeModerator', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomModerator', findResult.rid, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.removeOwner', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomOwner', findResult.rid, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.removeLeader', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('removeRoomLeader', findResult.rid, user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.rename', { authRequired: true }, { + post() { + if (!this.bodyParams.name || !this.bodyParams.name.trim()) { + return API.v1.failure('The bodyParam "name" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: { roomId: this.bodyParams.roomId }, userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomName', this.bodyParams.name); + }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(findResult.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.setCustomFields', { authRequired: true }, { + post() { + if (!this.bodyParams.customFields || !(typeof this.bodyParams.customFields === 'object')) { + return API.v1.failure('The bodyParam "customFields" is required with a type like object.'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomCustomFields', this.bodyParams.customFields); + }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(findResult.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.setDescription', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('description')) { + return API.v1.failure('The bodyParam "description" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomDescription', this.bodyParams.description); + }); + + return API.v1.success({ + description: this.bodyParams.description, + }); + }, +}); + +API.v1.addRoute('groups.setPurpose', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('purpose')) { + return API.v1.failure('The bodyParam "purpose" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomDescription', this.bodyParams.purpose); + }); + + return API.v1.success({ + purpose: this.bodyParams.purpose, + }); + }, +}); + +API.v1.addRoute('groups.setReadOnly', { authRequired: true }, { + post() { + if (typeof this.bodyParams.readOnly === 'undefined') { + return API.v1.failure('The bodyParam "readOnly" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + if (findResult.ro === this.bodyParams.readOnly) { + return API.v1.failure('The private group read only setting is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'readOnly', this.bodyParams.readOnly); + }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(findResult.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.setTopic', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('topic')) { + return API.v1.failure('The bodyParam "topic" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomTopic', this.bodyParams.topic); + }); + + return API.v1.success({ + topic: this.bodyParams.topic, + }); + }, +}); + +API.v1.addRoute('groups.setType', { authRequired: true }, { + post() { + if (!this.bodyParams.type || !this.bodyParams.type.trim()) { + return API.v1.failure('The bodyParam "type" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + if (findResult.t === this.bodyParams.type) { + return API.v1.failure('The private group type is the same as what it would be changed to.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomType', this.bodyParams.type); + }); + + return API.v1.success({ + group: this.composeRoomWithLastMessage(Rooms.findOneById(findResult.rid, { fields: API.v1.defaultFieldsToExclude }), this.userId), + }); + }, +}); + +API.v1.addRoute('groups.setAnnouncement', { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('announcement')) { + return API.v1.failure('The bodyParam "announcement" is required'); + } + + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.rid, 'roomAnnouncement', this.bodyParams.announcement); + }); + + return API.v1.success({ + announcement: this.bodyParams.announcement, + }); + }, +}); + +API.v1.addRoute('groups.unarchive', { authRequired: true }, { + post() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId, checkedArchived: false }); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('unarchiveRoom', findResult.rid); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('groups.roles', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const roles = Meteor.runAsUser(this.userId, () => Meteor.call('getRoomRoles', findResult.rid)); + + return API.v1.success({ + roles, + }); + }, +}); + +API.v1.addRoute('groups.moderators', { authRequired: true }, { + get() { + const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); + + const moderators = Subscriptions.findByRoomIdAndRoles(findResult.rid, ['moderator'], { fields: { u: 1 } }).fetch().map((sub) => sub.u); + + return API.v1.success({ + moderators, + }); + }, +}); diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js new file mode 100644 index 000000000000..5afd41d05cb1 --- /dev/null +++ b/app/api/server/v1/im.js @@ -0,0 +1,369 @@ +import { Meteor } from 'meteor/meteor'; + +import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib'; +import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models'; +import { hasPermission } from '../../../authorization'; +import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; +import { settings } from '../../../settings'; +import { API } from '../api'; + +function findDirectMessageRoom(params, user) { + if ((!params.roomId || !params.roomId.trim()) && (!params.username || !params.username.trim())) { + throw new Meteor.Error('error-room-param-not-provided', 'Body param "roomId" or "username" is required'); + } + + const room = getRoomByNameOrIdWithOptionToJoin({ + currentUserId: user._id, + nameOrId: params.username || params.roomId, + type: 'd', + }); + + const canAccess = Meteor.call('canAccessRoom', room._id, user._id); + if (!canAccess || !room || room.t !== 'd') { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "username" param provided does not match any dirct message'); + } + + const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); + + return { + room, + subscription, + }; +} + +API.v1.addRoute(['dm.create', 'im.create'], { authRequired: true }, { + post() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + return API.v1.success({ + room: findResult.room, + }); + }, +}); + +API.v1.addRoute(['dm.close', 'im.close'], { authRequired: true }, { + post() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + if (!findResult.subscription.open) { + return API.v1.failure(`The direct message room, ${ this.bodyParams.name }, is already closed to the sender`); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('hideRoom', findResult.room._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute(['dm.counters', 'im.counters'], { authRequired: true }, { + get() { + const access = hasPermission(this.userId, 'view-room-administration'); + const ruserId = this.requestParams().userId; + let user = this.userId; + let unreads = null; + let userMentions = null; + let unreadsFrom = null; + let joined = false; + let msgs = null; + let latest = null; + let members = null; + let lm = null; + + if (ruserId) { + if (!access) { + return API.v1.unauthorized(); + } + user = ruserId; + } + const rs = findDirectMessageRoom(this.requestParams(), { _id: user }); + const { room } = rs; + const dm = rs.subscription; + lm = room.lm ? room.lm : room._updatedAt; + + if (typeof dm !== 'undefined' && dm.open) { + if (dm.ls && room.msgs) { + unreads = dm.unread; + unreadsFrom = dm.ls; + } + userMentions = dm.userMentions; + joined = true; + } + + if (access || joined) { + msgs = room.msgs; + latest = lm; + members = room.usersCount; + } + + return API.v1.success({ + joined, + members, + unreads, + unreadsFrom, + msgs, + latest, + userMentions, + }); + }, +}); + +API.v1.addRoute(['dm.files', 'im.files'], { authRequired: true }, { + get() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + const addUserObjectToEveryObject = (file) => { + if (file.userId) { + file = this.insertUserObject({ object: file, userId: file.userId }); + } + return file; + }; + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult.room._id }); + + const files = Uploads.find(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + files: files.map(addUserObjectToEveryObject), + count: files.length, + offset, + total: Uploads.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute(['dm.history', 'im.history'], { authRequired: true }, { + get() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + let latestDate = new Date(); + if (this.queryParams.latest) { + latestDate = new Date(this.queryParams.latest); + } + + let oldestDate = undefined; + if (this.queryParams.oldest) { + oldestDate = new Date(this.queryParams.oldest); + } + + const inclusive = this.queryParams.inclusive || false; + + let count = 20; + if (this.queryParams.count) { + count = parseInt(this.queryParams.count); + } + + let offset = 0; + if (this.queryParams.offset) { + offset = parseInt(this.queryParams.offset); + } + + const unreads = this.queryParams.unreads || false; + + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('getChannelHistory', { + rid: findResult.room._id, + latest: latestDate, + oldest: oldestDate, + inclusive, + offset, + count, + unreads, + }); + }); + + if (!result) { + return API.v1.unauthorized(); + } + + return API.v1.success(result); + }, +}); + +API.v1.addRoute(['dm.members', 'im.members'], { authRequired: true }, { + get() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + const { offset, count } = this.getPaginationItems(); + const { sort } = this.parseJsonQuery(); + const cursor = Subscriptions.findByRoomId(findResult.room._id, { + sort: { 'u.username': sort && sort.username ? sort.username : 1 }, + skip: offset, + limit: count, + }); + + const total = cursor.count(); + const members = cursor.fetch().map((s) => s.u && s.u.username); + + const users = Users.find({ username: { $in: members } }, { + fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, + sort: { username: sort && sort.username ? sort.username : 1 }, + }).fetch(); + + return API.v1.success({ + members: users, + count: members.length, + offset, + total, + }); + }, +}); + +API.v1.addRoute(['dm.messages', 'im.messages'], { authRequired: true }, { + get() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { rid: findResult.room._id }); + + const messages = Messages.find(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + messages: normalizeMessagesForUser(messages, this.userId), + count: messages.length, + offset, + total: Messages.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute(['dm.messages.others', 'im.messages.others'], { authRequired: true }, { + get() { + if (settings.get('API_Enable_Direct_Message_History_EndPoint') !== true) { + throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { route: '/api/v1/im.messages.others' }); + } + + if (!hasPermission(this.userId, 'view-room-administration')) { + return API.v1.unauthorized(); + } + + const { roomId } = this.queryParams; + if (!roomId || !roomId.trim()) { + throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" is required'); + } + + const room = Rooms.findOneById(roomId); + if (!room || room.t !== 'd') { + throw new Meteor.Error('error-room-not-found', `No direct message room found by the id of: ${ roomId }`); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + const ourQuery = Object.assign({}, query, { rid: room._id }); + + const msgs = Messages.find(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + messages: normalizeMessagesForUser(msgs, this.userId), + offset, + count: msgs.length, + total: Messages.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute(['dm.list', 'im.list'], { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort = { name: 1 }, fields } = this.parseJsonQuery(); + + // TODO: CACHE: Add Breacking notice since we removed the query param + + const cursor = Rooms.findBySubscriptionTypeAndUserId('d', this.userId, { + sort, + skip: offset, + limit: count, + fields, + }); + + const total = cursor.count(); + const rooms = cursor.fetch(); + + return API.v1.success({ + ims: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + offset, + count: rooms.length, + total, + }); + }, +}); + +API.v1.addRoute(['dm.list.everyone', 'im.list.everyone'], { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'view-room-administration')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { t: 'd' }); + + const rooms = Rooms.find(ourQuery, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + ims: rooms.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + offset, + count: rooms.length, + total: Rooms.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute(['dm.open', 'im.open'], { authRequired: true }, { + post() { + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + if (!findResult.subscription.open) { + Meteor.runAsUser(this.userId, () => { + Meteor.call('openRoom', findResult.room._id); + }); + } + + return API.v1.success(); + }, +}); + +API.v1.addRoute(['dm.setTopic', 'im.setTopic'], { authRequired: true }, { + post() { + if (!this.bodyParams.hasOwnProperty('topic')) { + return API.v1.failure('The bodyParam "topic" is required'); + } + + const findResult = findDirectMessageRoom(this.requestParams(), this.user); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('saveRoomSettings', findResult.room._id, 'roomTopic', this.bodyParams.topic); + }); + + return API.v1.success({ + topic: this.bodyParams.topic, + }); + }, +}); diff --git a/app/api/server/v1/import.js b/app/api/server/v1/import.js new file mode 100644 index 000000000000..0ee09ae5789c --- /dev/null +++ b/app/api/server/v1/import.js @@ -0,0 +1,51 @@ +import { Meteor } from 'meteor/meteor'; + +import { API } from '../api'; + +API.v1.addRoute('uploadImportFile', { authRequired: true }, { + post() { + const { binaryContent, contentType, fileName, importerKey } = this.bodyParams; + + Meteor.runAsUser(this.userId, () => { + API.v1.success(Meteor.call('uploadImportFile', binaryContent, contentType, fileName, importerKey)); + }); + + return API.v1.success(); + }, + +}); + +API.v1.addRoute('downloadPublicImportFile', { authRequired: true }, { + post() { + const { fileUrl, importerKey } = this.bodyParams; + + Meteor.runAsUser(this.userId, () => { + API.v1.success(Meteor.call('downloadPublicImportFile', fileUrl, importerKey)); + }); + + return API.v1.success(); + }, + +}); + +API.v1.addRoute('getImportFileData', { authRequired: true }, { + get() { + const { importerKey } = this.requestParams(); + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('getImportFileData', importerKey); + }); + + return API.v1.success(result); + }, + +}); + +API.v1.addRoute('getLatestImportOperations', { authRequired: true }, { + get() { + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('getLatestImportOperations'); }); + + return API.v1.success(result); + }, +}); diff --git a/app/api/server/v1/integrations.js b/app/api/server/v1/integrations.js new file mode 100644 index 000000000000..b4e69cbf1489 --- /dev/null +++ b/app/api/server/v1/integrations.js @@ -0,0 +1,156 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; + +import { hasPermission } from '../../../authorization'; +import { IntegrationHistory, Integrations } from '../../../models'; +import { API } from '../api'; + +API.v1.addRoute('integrations.create', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + type: String, + name: String, + enabled: Boolean, + username: String, + urls: Match.Maybe([String]), + channel: String, + event: Match.Maybe(String), + triggerWords: Match.Maybe([String]), + alias: Match.Maybe(String), + avatar: Match.Maybe(String), + emoji: Match.Maybe(String), + token: Match.Maybe(String), + scriptEnabled: Boolean, + script: Match.Maybe(String), + targetChannel: Match.Maybe(String), + })); + + let integration; + + switch (this.bodyParams.type) { + case 'webhook-outgoing': + Meteor.runAsUser(this.userId, () => { + integration = Meteor.call('addOutgoingIntegration', this.bodyParams); + }); + break; + case 'webhook-incoming': + Meteor.runAsUser(this.userId, () => { + integration = Meteor.call('addIncomingIntegration', this.bodyParams); + }); + break; + default: + return API.v1.failure('Invalid integration type.'); + } + + return API.v1.success({ integration }); + }, +}); + +API.v1.addRoute('integrations.history', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'manage-integrations')) { + return API.v1.unauthorized(); + } + + if (!this.queryParams.id || this.queryParams.id.trim() === '') { + return API.v1.failure('Invalid integration id.'); + } + + const { id } = this.queryParams; + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { 'integration._id': id }); + const history = IntegrationHistory.find(ourQuery, { + sort: sort || { _updatedAt: -1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + history, + offset, + items: history.length, + total: IntegrationHistory.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('integrations.list', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'manage-integrations')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query); + const integrations = Integrations.find(ourQuery, { + sort: sort || { ts: -1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + integrations, + offset, + items: integrations.length, + total: Integrations.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('integrations.remove', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + type: String, + target_url: Match.Maybe(String), + integrationId: Match.Maybe(String), + })); + + if (!this.bodyParams.target_url && !this.bodyParams.integrationId) { + return API.v1.failure('An integrationId or target_url needs to be provided.'); + } + + let integration; + switch (this.bodyParams.type) { + case 'webhook-outgoing': + if (this.bodyParams.target_url) { + integration = Integrations.findOne({ urls: this.bodyParams.target_url }); + } else if (this.bodyParams.integrationId) { + integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + } + + if (!integration) { + return API.v1.failure('No integration found.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('deleteOutgoingIntegration', integration._id); + }); + + return API.v1.success({ + integration, + }); + case 'webhook-incoming': + integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + + if (!integration) { + return API.v1.failure('No integration found.'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('deleteIncomingIntegration', integration._id); + }); + + return API.v1.success({ + integration, + }); + default: + return API.v1.failure('Invalid integration type.'); + } + }, +}); diff --git a/app/api/server/v1/misc.js b/app/api/server/v1/misc.js new file mode 100644 index 000000000000..8aa8f7b973b5 --- /dev/null +++ b/app/api/server/v1/misc.js @@ -0,0 +1,204 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import s from 'underscore.string'; + +import { hasRole } from '../../../authorization'; +import { Info } from '../../../utils'; +import { Users } from '../../../models'; +import { settings } from '../../../settings'; +import { API } from '../api'; +import { getDefaultUserFields } from '../../../utils/server/functions/getDefaultUserFields'; +import { getURL } from '../../../utils/lib/getURL'; + + +// DEPRECATED +// Will be removed after v1.12.0 +API.v1.addRoute('info', { authRequired: false }, { + get() { + const warningMessage = 'The endpoint "/v1/info" is deprecated and will be removed after version v1.12.0'; + console.warn(warningMessage); + const user = this.getLoggedInUser(); + + if (user && hasRole(user._id, 'admin')) { + return API.v1.success(this.deprecationWarning({ + endpoint: 'info', + versionWillBeRemoved: '1.12.0', + response: { + info: Info, + }, + })); + } + + return API.v1.success(this.deprecationWarning({ + endpoint: 'info', + versionWillBeRemoved: '1.12.0', + response: { + info: { + version: Info.version, + }, + }, + })); + }, +}); + +API.v1.addRoute('me', { authRequired: true }, { + get() { + return API.v1.success(this.getUserInfo(Users.findOneById(this.userId, { fields: getDefaultUserFields() }))); + }, +}); + +let onlineCache = 0; +let onlineCacheDate = 0; +const cacheInvalid = 60000; // 1 minute +API.v1.addRoute('shield.svg', { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 60, intervalTimeInMS: 60000 } }, { + get() { + const { type, icon } = this.queryParams; + let { channel, name } = this.queryParams; + if (!settings.get('API_Enable_Shields')) { + throw new Meteor.Error('error-endpoint-disabled', 'This endpoint is disabled', { route: '/api/v1/shield.svg' }); + } + + const types = settings.get('API_Shield_Types'); + if (type && (types !== '*' && !types.split(',').map((t) => t.trim()).includes(type))) { + throw new Meteor.Error('error-shield-disabled', 'This shield type is disabled', { route: '/api/v1/shield.svg' }); + } + const hideIcon = icon === 'false'; + if (hideIcon && (!name || !name.trim())) { + return API.v1.failure('Name cannot be empty when icon is hidden'); + } + + let text; + let backgroundColor = '#4c1'; + switch (type) { + case 'online': + if (Date.now() - onlineCacheDate > cacheInvalid) { + onlineCache = Users.findUsersNotOffline().count(); + onlineCacheDate = Date.now(); + } + + text = `${ onlineCache } ${ TAPi18n.__('Online') }`; + break; + case 'channel': + if (!channel) { + return API.v1.failure('Shield channel is required for type "channel"'); + } + + text = `#${ channel }`; + break; + case 'user': + const user = this.getUserFromParams(); + + // Respect the server's choice for using their real names or not + if (user.name && settings.get('UI_Use_Real_Name')) { + text = `${ user.name }`; + } else { + text = `@${ user.username }`; + } + + switch (user.status) { + case 'online': + backgroundColor = '#1fb31f'; + break; + case 'away': + backgroundColor = '#dc9b01'; + break; + case 'busy': + backgroundColor = '#bc2031'; + break; + case 'offline': + backgroundColor = '#a5a1a1'; + } + break; + default: + text = TAPi18n.__('Join_Chat').toUpperCase(); + } + + const iconSize = hideIcon ? 7 : 24; + const leftSize = name ? name.length * 6 + 7 + iconSize : iconSize; + const rightSize = text.length * 6 + 20; + const width = leftSize + rightSize; + const height = 20; + + channel = s.escapeHTML(channel); + text = s.escapeHTML(text); + name = s.escapeHTML(name); + + return { + headers: { 'Content-Type': 'image/svg+xml;charset=utf-8' }, + body: ` + + + + + + + + + + + + + + ${ hideIcon ? '' : `` } + + ${ name ? `${ name } + ${ name }` : '' } + ${ text } + ${ text } + + + `.trim().replace(/\>[\s]+\<'), + }; + }, +}); + +API.v1.addRoute('spotlight', { authRequired: true }, { + get() { + check(this.queryParams, { + query: String, + }); + + const { query } = this.queryParams; + + const result = Meteor.runAsUser(this.userId, () => + Meteor.call('spotlight', query) + ); + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('directory', { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, query } = this.parseJsonQuery(); + + const { text, type, workspace = 'local' } = query; + if (sort && Object.keys(sort).length > 1) { + return API.v1.failure('This method support only one "sort" parameter'); + } + const sortBy = sort ? Object.keys(sort)[0] : undefined; + const sortDirection = sort && Object.values(sort)[0] === 1 ? 'asc' : 'desc'; + + const result = Meteor.runAsUser(this.userId, () => Meteor.call('browseChannels', { + text, + type, + workspace, + sortBy, + sortDirection, + offset: Math.max(0, offset), + limit: Math.max(0, count), + })); + + if (!result) { + return API.v1.failure('Please verify the parameters'); + } + return API.v1.success({ + result: result.results, + count: result.results.length, + offset, + total: result.total, + }); + }, +}); diff --git a/app/api/server/v1/permissions.js b/app/api/server/v1/permissions.js new file mode 100644 index 000000000000..c5d790533287 --- /dev/null +++ b/app/api/server/v1/permissions.js @@ -0,0 +1,120 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; + +import { hasPermission } from '../../../authorization'; +import { Permissions, Roles } from '../../../models'; +import { API } from '../api'; + +/** + This API returns all permissions that exists + on the server, with respective roles. + + Method: GET + Route: api/v1/permissions + */ +API.v1.addRoute('permissions', { authRequired: true }, { + get() { + const warningMessage = 'The endpoint "permissions" is deprecated and will be removed after version v0.69'; + console.warn(warningMessage); + + const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); + + return API.v1.success(result); + }, +}); + +// DEPRECATED +// TODO: Remove this after three versions have been released. That means at 0.85 this should be gone. +API.v1.addRoute('permissions.list', { authRequired: true }, { + get() { + const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); + + return API.v1.success(this.deprecationWarning({ + endpoint: 'permissions.list', + versionWillBeRemoved: '0.85', + response: { + permissions: result, + }, + })); + }, +}); + +API.v1.addRoute('permissions.listAll', { authRequired: true }, { + get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + } + + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('permissions/get', updatedSinceDate); }); + + if (Array.isArray(result)) { + result = { + update: result, + remove: [], + }; + } + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('permissions.update', { authRequired: true }, { + post() { + if (!hasPermission(this.userId, 'access-permissions')) { + return API.v1.failure('Editing permissions is not allowed', 'error-edit-permissions-not-allowed'); + } + + check(this.bodyParams, { + permissions: [ + Match.ObjectIncluding({ + _id: String, + roles: [String], + }), + ], + }); + + let permissionNotFound = false; + let roleNotFound = false; + Object.keys(this.bodyParams.permissions).forEach((key) => { + const element = this.bodyParams.permissions[key]; + + if (!Permissions.findOneById(element._id)) { + permissionNotFound = true; + } + + Object.keys(element.roles).forEach((key) => { + const subelement = element.roles[key]; + + if (!Roles.findOneById(subelement)) { + roleNotFound = true; + } + }); + }); + + if (permissionNotFound) { + return API.v1.failure('Invalid permission', 'error-invalid-permission'); + } if (roleNotFound) { + return API.v1.failure('Invalid role', 'error-invalid-role'); + } + + Object.keys(this.bodyParams.permissions).forEach((key) => { + const element = this.bodyParams.permissions[key]; + + Permissions.createOrUpdate(element._id, element.roles); + }); + + const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); + + return API.v1.success({ + permissions: result, + }); + }, +}); diff --git a/app/api/server/v1/push.js b/app/api/server/v1/push.js new file mode 100644 index 000000000000..6c1c9959011a --- /dev/null +++ b/app/api/server/v1/push.js @@ -0,0 +1,65 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; +import { Push } from 'meteor/rocketchat:push'; + +import { API } from '../api'; + +API.v1.addRoute('push.token', { authRequired: true }, { + post() { + const { type, value, appName } = this.bodyParams; + let { id } = this.bodyParams; + + if (id && typeof id !== 'string') { + throw new Meteor.Error('error-id-param-not-valid', 'The required "id" body param is invalid.'); + } else { + id = Random.id(); + } + + if (!type || (type !== 'apn' && type !== 'gcm')) { + throw new Meteor.Error('error-type-param-not-valid', 'The required "type" body param is missing or invalid.'); + } + + if (!value || typeof value !== 'string') { + throw new Meteor.Error('error-token-param-not-valid', 'The required "value" body param is missing or invalid.'); + } + + if (!appName || typeof appName !== 'string') { + throw new Meteor.Error('error-appName-param-not-valid', 'The required "appName" body param is missing or invalid.'); + } + + + let result; + Meteor.runAsUser(this.userId, () => { + result = Meteor.call('raix:push-update', { + id, + token: { [type]: value }, + appName, + userId: this.userId, + }); + }); + + return API.v1.success({ result }); + }, + delete() { + const { token } = this.bodyParams; + + if (!token || typeof token !== 'string') { + throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.'); + } + + const affectedRecords = Push.appCollection.remove({ + $or: [{ + 'token.apn': token, + }, { + 'token.gcm': token, + }], + userId: this.userId, + }); + + if (affectedRecords === 0) { + return API.v1.notFound(); + } + + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/roles.js b/app/api/server/v1/roles.js new file mode 100644 index 000000000000..22a0d543ce34 --- /dev/null +++ b/app/api/server/v1/roles.js @@ -0,0 +1,88 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; + +import { Roles } from '../../../models'; +import { API } from '../api'; +import { getUsersInRole, hasPermission } from '../../../authorization/server'; + +API.v1.addRoute('roles.list', { authRequired: true }, { + get() { + const roles = Roles.find({}, { fields: { _updatedAt: 0 } }).fetch(); + + return API.v1.success({ roles }); + }, +}); + +API.v1.addRoute('roles.create', { authRequired: true }, { + post() { + check(this.bodyParams, { + name: String, + scope: Match.Maybe(String), + description: Match.Maybe(String), + }); + + const roleData = { + name: this.bodyParams.name, + scope: this.bodyParams.scope, + description: this.bodyParams.description, + }; + + Meteor.runAsUser(this.userId, () => { + Meteor.call('authorization:saveRole', roleData); + }); + + return API.v1.success({ + role: Roles.findOneByIdOrName(roleData.name, { fields: API.v1.defaultFieldsToExclude }), + }); + }, +}); + +API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { + post() { + check(this.bodyParams, { + roleName: String, + username: String, + roomId: Match.Maybe(String), + }); + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('authorization:addUserToRole', this.bodyParams.roleName, user.username, this.bodyParams.roomId); + }); + + return API.v1.success({ + role: Roles.findOneByIdOrName(this.bodyParams.roleName, { fields: API.v1.defaultFieldsToExclude }), + }); + }, +}); + +API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { + get() { + const { roomId, role } = this.queryParams; + const { offset, count = 50 } = this.getPaginationItems(); + + const fields = { + name: 1, + username: 1, + emails: 1, + }; + + if (!role) { + throw new Meteor.Error('error-param-not-provided', 'Query param "role" is required'); + } + if (!hasPermission(this.userId, 'access-permissions')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + if (roomId && !hasPermission(this.userId, 'view-other-user-channels')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + const users = getUsersInRole(role, roomId, { + limit: count, + sort: { username: 1 }, + skip: offset, + fields, + }).fetch(); + return API.v1.success({ users }); + }, +}); diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js new file mode 100644 index 000000000000..7cab7a10977b --- /dev/null +++ b/app/api/server/v1/rooms.js @@ -0,0 +1,272 @@ +import { Meteor } from 'meteor/meteor'; +import Busboy from 'busboy'; + +import { FileUpload } from '../../../file-upload'; +import { Rooms } from '../../../models'; +import { API } from '../api'; + +function findRoomByIdOrName({ params, checkedArchived = true }) { + if ((!params.roomId || !params.roomId.trim()) && (!params.roomName || !params.roomName.trim())) { + throw new Meteor.Error('error-roomid-param-not-provided', 'The parameter "roomId" or "roomName" is required'); + } + + const fields = { ...API.v1.defaultFieldsToExclude }; + + let room; + if (params.roomId) { + room = Rooms.findOneById(params.roomId, { fields }); + } else if (params.roomName) { + room = Rooms.findOneByName(params.roomName, { fields }); + } + if (!room) { + throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any channel'); + } + if (checkedArchived && room.archived) { + throw new Meteor.Error('error-room-archived', `The channel, ${ room.name }, is archived`); + } + + return room; +} + +API.v1.addRoute('rooms.get', { authRequired: true }, { + get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + } + + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('rooms/get', updatedSinceDate); }); + + if (Array.isArray(result)) { + result = { + update: result, + remove: [], + }; + } + + return API.v1.success({ + update: result.update.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + remove: result.remove.map((room) => this.composeRoomWithLastMessage(room, this.userId)), + }); + }, +}); + +API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { + post() { + const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); + + if (!room) { + return API.v1.unauthorized(); + } + + const busboy = new Busboy({ headers: this.request.headers }); + const files = []; + const fields = {}; + + Meteor.wrapAsync((callback) => { + busboy.on('file', (fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'file') { + return files.push(new Meteor.Error('invalid-field')); + } + + const fileDate = []; + file.on('data', (data) => fileDate.push(data)); + + file.on('end', () => { + files.push({ fieldname, file, filename, encoding, mimetype, fileBuffer: Buffer.concat(fileDate) }); + }); + }); + + busboy.on('field', (fieldname, value) => { fields[fieldname] = value; }); + + busboy.on('finish', Meteor.bindEnvironment(() => callback())); + + this.request.pipe(busboy); + })(); + + if (files.length === 0) { + return API.v1.failure('File required'); + } + + if (files.length > 1) { + return API.v1.failure('Just 1 file is allowed'); + } + + const file = files[0]; + + const fileStore = FileUpload.getStore('Uploads'); + + const details = { + name: file.filename, + size: file.fileBuffer.length, + type: file.mimetype, + rid: this.urlParams.rid, + userId: this.userId, + }; + + Meteor.runAsUser(this.userId, () => { + const uploadedFile = Meteor.wrapAsync(fileStore.insert.bind(fileStore))(details, file.fileBuffer); + + uploadedFile.description = fields.description; + + delete fields.description; + + API.v1.success(Meteor.call('sendFileMessage', this.urlParams.rid, null, uploadedFile, fields)); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('rooms.saveNotification', { authRequired: true }, { + post() { + const saveNotifications = (notifications, roomId) => { + Object.keys(notifications).forEach((notificationKey) => + Meteor.runAsUser(this.userId, () => + Meteor.call('saveNotificationSettings', roomId, notificationKey, notifications[notificationKey]) + ) + ); + }; + const { roomId, notifications } = this.bodyParams; + + if (!roomId) { + return API.v1.failure('The \'roomId\' param is required'); + } + + if (!notifications || Object.keys(notifications).length === 0) { + return API.v1.failure('The \'notifications\' param is required'); + } + + saveNotifications(notifications, roomId); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('rooms.favorite', { authRequired: true }, { + post() { + const { favorite } = this.bodyParams; + + if (!this.bodyParams.hasOwnProperty('favorite')) { + return API.v1.failure('The \'favorite\' param is required'); + } + + const room = findRoomByIdOrName({ params: this.bodyParams }); + + Meteor.runAsUser(this.userId, () => Meteor.call('toggleFavorite', room._id, favorite)); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('rooms.cleanHistory', { authRequired: true }, { + post() { + const findResult = findRoomByIdOrName({ params: this.bodyParams }); + + if (!this.bodyParams.latest) { + return API.v1.failure('Body parameter "latest" is required.'); + } + + if (!this.bodyParams.oldest) { + return API.v1.failure('Body parameter "oldest" is required.'); + } + + const latest = new Date(this.bodyParams.latest); + const oldest = new Date(this.bodyParams.oldest); + + const inclusive = this.bodyParams.inclusive || false; + + Meteor.runAsUser(this.userId, () => Meteor.call('cleanRoomHistory', { + roomId: findResult._id, + latest, + oldest, + inclusive, + limit: this.bodyParams.limit, + excludePinned: this.bodyParams.excludePinned, + filesOnly: this.bodyParams.filesOnly, + fromUsers: this.bodyParams.users, + })); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('rooms.info', { authRequired: true }, { + get() { + const room = findRoomByIdOrName({ params: this.requestParams() }); + const { fields } = this.parseJsonQuery(); + if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + return API.v1.failure('not-allowed', 'Not Allowed'); + } + return API.v1.success({ room: Rooms.findOneByIdOrName(room._id, { fields }) }); + }, +}); + +API.v1.addRoute('rooms.leave', { authRequired: true }, { + post() { + const room = findRoomByIdOrName({ params: this.bodyParams }); + Meteor.runAsUser(this.userId, () => { + Meteor.call('leaveRoom', room._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('rooms.createDiscussion', { authRequired: true }, { + post() { + const { prid, pmid, reply, t_name, users } = this.bodyParams; + if (!prid) { + return API.v1.failure('Body parameter "prid" is required.'); + } + if (!t_name) { + return API.v1.failure('Body parameter "t_name" is required.'); + } + if (users && !Array.isArray(users)) { + return API.v1.failure('Body parameter "users" must be an array.'); + } + + const discussion = Meteor.runAsUser(this.userId, () => Meteor.call('createDiscussion', { + prid, + pmid, + t_name, + reply, + users: users || [], + })); + + return API.v1.success({ discussion }); + }, +}); + +API.v1.addRoute('rooms.getDiscussions', { authRequired: true }, { + get() { + const room = findRoomByIdOrName({ params: this.requestParams() }); + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + return API.v1.failure('not-allowed', 'Not Allowed'); + } + const ourQuery = Object.assign(query, { prid: room._id }); + + const discussions = Rooms.find(ourQuery, { + sort: sort || { fname: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + discussions, + count: discussions.length, + offset, + total: Rooms.find(ourQuery).count(), + }); + }, +}); diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.js new file mode 100644 index 000000000000..1fd8dba80c0b --- /dev/null +++ b/app/api/server/v1/settings.js @@ -0,0 +1,142 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; +import { ServiceConfiguration } from 'meteor/service-configuration'; +import _ from 'underscore'; + +import { Settings } from '../../../models'; +import { hasPermission } from '../../../authorization'; +import { API } from '../api'; + +// settings endpoints +API.v1.addRoute('settings.public', { authRequired: false }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + let ourQuery = { + hidden: { $ne: true }, + public: true, + }; + + ourQuery = Object.assign({}, query, ourQuery); + + const settings = Settings.find(ourQuery, { + sort: sort || { _id: 1 }, + skip: offset, + limit: count, + fields: Object.assign({ _id: 1, value: 1 }, fields), + }).fetch(); + + return API.v1.success({ + settings, + count: settings.length, + offset, + total: Settings.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('settings.oauth', { authRequired: false }, { + get() { + const mountOAuthServices = () => { + const oAuthServicesEnabled = ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); + + return oAuthServicesEnabled.map((service) => { + if (service.custom || ['saml', 'cas', 'wordpress'].includes(service.service)) { + return { ...service }; + } + + return { + _id: service._id, + name: service.service, + clientId: service.appId || service.clientId || service.consumerKey, + buttonLabelText: service.buttonLabelText || '', + buttonColor: service.buttonColor || '', + buttonLabelColor: service.buttonLabelColor || '', + custom: false, + }; + }); + }; + + return API.v1.success({ + services: mountOAuthServices(), + }); + }, +}); + +API.v1.addRoute('settings', { authRequired: true }, { + get() { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + let ourQuery = { + hidden: { $ne: true }, + }; + + if (!hasPermission(this.userId, 'view-privileged-setting')) { + ourQuery.public = true; + } + + ourQuery = Object.assign({}, query, ourQuery); + + const settings = Settings.find(ourQuery, { + sort: sort || { _id: 1 }, + skip: offset, + limit: count, + fields: Object.assign({ _id: 1, value: 1 }, fields), + }).fetch(); + + return API.v1.success({ + settings, + count: settings.length, + offset, + total: Settings.find(ourQuery).count(), + }); + }, +}); + +API.v1.addRoute('settings/:_id', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'view-privileged-setting')) { + return API.v1.unauthorized(); + } + + return API.v1.success(_.pick(Settings.findOneNotHiddenById(this.urlParams._id), '_id', 'value')); + }, + post() { + if (!hasPermission(this.userId, 'edit-privileged-setting')) { + return API.v1.unauthorized(); + } + + // allow special handling of particular setting types + const setting = Settings.findOneNotHiddenById(this.urlParams._id); + if (setting.type === 'action' && this.bodyParams && this.bodyParams.execute) { + // execute the configured method + Meteor.call(setting.value); + return API.v1.success(); + } + + if (setting.type === 'color' && this.bodyParams && this.bodyParams.editor && this.bodyParams.value) { + Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); + Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); + return API.v1.success(); + } + + check(this.bodyParams, { + value: Match.Any, + }); + if (Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) { + return API.v1.success(); + } + + return API.v1.failure(); + }, +}); + +API.v1.addRoute('service.configurations', { authRequired: false }, { + get() { + return API.v1.success({ + configurations: ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(), + }); + }, +}); diff --git a/app/api/server/v1/stats.js b/app/api/server/v1/stats.js new file mode 100644 index 000000000000..2f74a2092947 --- /dev/null +++ b/app/api/server/v1/stats.js @@ -0,0 +1,48 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../../../authorization'; +import { Statistics } from '../../../models'; +import { API } from '../api'; + +API.v1.addRoute('statistics', { authRequired: true }, { + get() { + let refresh = false; + if (typeof this.queryParams.refresh !== 'undefined' && this.queryParams.refresh === 'true') { + refresh = true; + } + + let stats; + Meteor.runAsUser(this.userId, () => { + stats = Meteor.call('getStatistics', refresh); + }); + + return API.v1.success({ + statistics: stats, + }); + }, +}); + +API.v1.addRoute('statistics.list', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'view-statistics')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const statistics = Statistics.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + statistics, + count: statistics.length, + offset, + total: Statistics.find(query).count(), + }); + }, +}); diff --git a/app/api/server/v1/subscriptions.js b/app/api/server/v1/subscriptions.js new file mode 100644 index 000000000000..8ffa8711a335 --- /dev/null +++ b/app/api/server/v1/subscriptions.js @@ -0,0 +1,85 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { Subscriptions } from '../../../models'; +import { API } from '../api'; + +API.v1.addRoute('subscriptions.get', { authRequired: true }, { + get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); + } else { + updatedSinceDate = new Date(updatedSince); + } + } + + let result; + Meteor.runAsUser(this.userId, () => { result = Meteor.call('subscriptions/get', updatedSinceDate); }); + + if (Array.isArray(result)) { + result = { + update: result, + remove: [], + }; + } + + return API.v1.success(result); + }, +}); + +API.v1.addRoute('subscriptions.getOne', { authRequired: true }, { + get() { + const { roomId } = this.requestParams(); + + if (!roomId) { + return API.v1.failure('The \'roomId\' param is required'); + } + + const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId); + + return API.v1.success({ + subscription, + }); + }, +}); + +/** + This API is suppose to mark any room as read. + + Method: POST + Route: api/v1/subscriptions.read + Params: + - rid: The rid of the room to be marked as read. + */ +API.v1.addRoute('subscriptions.read', { authRequired: true }, { + post() { + check(this.bodyParams, { + rid: String, + }); + + Meteor.runAsUser(this.userId, () => + Meteor.call('readMessages', this.bodyParams.rid) + ); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('subscriptions.unread', { authRequired: true }, { + post() { + const { roomId, firstUnreadMessage } = this.bodyParams; + if (!roomId && (firstUnreadMessage && !firstUnreadMessage._id)) { + return API.v1.failure('At least one of "roomId" or "firstUnreadMessage._id" params is required'); + } + + Meteor.runAsUser(this.userId, () => + Meteor.call('unreadMessages', firstUnreadMessage, roomId) + ); + + return API.v1.success(); + }, +}); diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js new file mode 100644 index 000000000000..602605d9c4d6 --- /dev/null +++ b/app/api/server/v1/users.js @@ -0,0 +1,676 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import _ from 'underscore'; +import Busboy from 'busboy'; + +import { Users, Subscriptions } from '../../../models/server'; +import { hasPermission } from '../../../authorization'; +import { settings } from '../../../settings'; +import { getURL } from '../../../utils'; +import { + validateCustomFields, + saveUser, + saveCustomFieldsWithoutValidation, + checkUsernameAvailability, + setUserAvatar, + saveCustomFields, +} from '../../../lib'; +import { getFullUserData } from '../../../lib/server/functions/getFullUserData'; +import { API } from '../api'; +import { setStatusText } from '../../../lib/server'; + +API.v1.addRoute('users.create', { authRequired: true }, { + post() { + check(this.bodyParams, { + email: String, + name: String, + password: String, + username: String, + active: Match.Maybe(Boolean), + roles: Match.Maybe(Array), + joinDefaultChannels: Match.Maybe(Boolean), + requirePasswordChange: Match.Maybe(Boolean), + sendWelcomeEmail: Match.Maybe(Boolean), + verified: Match.Maybe(Boolean), + customFields: Match.Maybe(Object), + }); + + // New change made by pull request #5152 + if (typeof this.bodyParams.joinDefaultChannels === 'undefined') { + this.bodyParams.joinDefaultChannels = true; + } + + if (this.bodyParams.customFields) { + validateCustomFields(this.bodyParams.customFields); + } + + const newUserId = saveUser(this.userId, this.bodyParams); + + if (this.bodyParams.customFields) { + saveCustomFieldsWithoutValidation(newUserId, this.bodyParams.customFields); + } + + + if (typeof this.bodyParams.active !== 'undefined') { + Meteor.runAsUser(this.userId, () => { + Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active); + }); + } + + return API.v1.success({ user: Users.findOneById(newUserId, { fields: API.v1.defaultFieldsToExclude }) }); + }, +}); + +API.v1.addRoute('users.delete', { authRequired: true }, { + post() { + if (!hasPermission(this.userId, 'delete-user')) { + return API.v1.unauthorized(); + } + + const user = this.getUserFromParams(); + + Meteor.runAsUser(this.userId, () => { + Meteor.call('deleteUser', user._id); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('users.deleteOwnAccount', { authRequired: true }, { + post() { + const { password } = this.bodyParams; + if (!password) { + return API.v1.failure('Body parameter "password" is required.'); + } + if (!settings.get('Accounts_AllowDeleteOwnAccount')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('deleteUserOwnAccount', password); + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('users.getAvatar', { authRequired: false }, { + get() { + const user = this.getUserFromParams(); + + const url = getURL(`/avatar/${ user.username }`, { cdn: false, full: true }); + this.response.setHeader('Location', url); + + return { + statusCode: 307, + body: url, + }; + }, +}); + +API.v1.addRoute('users.setActiveStatus', { authRequired: true }, { + post() { + check(this.bodyParams, { + userId: String, + activeStatus: Boolean, + }); + + if (!hasPermission(this.userId, 'edit-other-user-active-status')) { + return API.v1.unauthorized(); + } + + Meteor.runAsUser(this.userId, () => { + Meteor.call('setUserActiveStatus', this.bodyParams.userId, this.bodyParams.activeStatus); + }); + return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields: { active: 1 } }) }); + }, +}); + +API.v1.addRoute('users.getPresence', { authRequired: true }, { + get() { + if (this.isUserFromParams()) { + const user = Users.findOneById(this.userId); + return API.v1.success({ + presence: user.status, + connectionStatus: user.statusConnection, + lastLogin: user.lastLogin, + }); + } + + const user = this.getUserFromParams(); + + return API.v1.success({ + presence: user.status, + }); + }, +}); + +API.v1.addRoute('users.info', { authRequired: true }, { + get() { + const { username } = this.getUserFromParams(); + const { fields } = this.parseJsonQuery(); + + const result = getFullUserData({ + userId: this.userId, + filter: username, + limit: 1, + }); + if (!result || result.count() !== 1) { + return API.v1.failure(`Failed to get the user data for the userId of "${ this.userId }".`); + } + const [user] = result.fetch(); + const myself = user._id === this.userId; + if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) { + user.rooms = Subscriptions.findByUserId(user._id, { + fields: { + rid: 1, + name: 1, + t: 1, + roles: 1, + }, + sort: { + t: 1, + name: 1, + }, + }).fetch(); + } + + return API.v1.success({ + user, + }); + }, +}); + +API.v1.addRoute('users.list', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'view-d-room')) { + return API.v1.unauthorized(); + } + + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const users = Users.find(query, { + sort: sort || { username: 1 }, + skip: offset, + limit: count, + fields, + }).fetch(); + + return API.v1.success({ + users, + count: users.length, + offset, + total: Users.find(query).count(), + }); + }, +}); + +API.v1.addRoute('users.register', { authRequired: false }, { + post() { + if (this.userId) { + return API.v1.failure('Logged in users can not register again.'); + } + + // We set their username here, so require it + // The `registerUser` checks for the other requirements + check(this.bodyParams, Match.ObjectIncluding({ + username: String, + })); + + if (!checkUsernameAvailability(this.bodyParams.username)) { + return API.v1.failure('Username is already in use'); + } + + // Register the user + const userId = Meteor.call('registerUser', this.bodyParams); + + // Now set their username + Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username)); + + return API.v1.success({ user: Users.findOneById(userId, { fields: API.v1.defaultFieldsToExclude }) }); + }, +}); + +API.v1.addRoute('users.resetAvatar', { authRequired: true }, { + post() { + const user = this.getUserFromParams(); + + if (user._id === this.userId) { + Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar')); + } else if (hasPermission(this.userId, 'edit-other-user-info')) { + Meteor.runAsUser(user._id, () => Meteor.call('resetAvatar')); + } else { + return API.v1.unauthorized(); + } + + return API.v1.success(); + }, +}); + +API.v1.addRoute('users.setAvatar', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + avatarUrl: Match.Maybe(String), + userId: Match.Maybe(String), + username: Match.Maybe(String), + })); + + if (!settings.get('Accounts_AllowUserAvatarChange')) { + throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { + method: 'users.setAvatar', + }); + } + + let user; + if (this.isUserFromParams()) { + user = Meteor.users.findOne(this.userId); + } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { + user = this.getUserFromParams(); + } else { + return API.v1.unauthorized(); + } + + Meteor.runAsUser(user._id, () => { + if (this.bodyParams.avatarUrl) { + setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); + } else { + const busboy = new Busboy({ headers: this.request.headers }); + const fields = {}; + const getUserFromFormData = (fields) => { + if (fields.userId) { + return Users.findOneById(fields.userId, { _id: 1 }); + } + if (fields.username) { + return Users.findOneByUsernameIgnoringCase(fields.username, { _id: 1 }); + } + }; + + Meteor.wrapAsync((callback) => { + busboy.on('file', Meteor.bindEnvironment((fieldname, file, filename, encoding, mimetype) => { + if (fieldname !== 'image') { + return callback(new Meteor.Error('invalid-field')); + } + const imageData = []; + file.on('data', Meteor.bindEnvironment((data) => { + imageData.push(data); + })); + + file.on('end', Meteor.bindEnvironment(() => { + const sentTheUserByFormData = fields.userId || fields.username; + if (sentTheUserByFormData) { + user = getUserFromFormData(fields); + if (!user) { + return callback(new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users')); + } + const isAnotherUser = this.userId !== user._id; + if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-info')) { + return callback(new Meteor.Error('error-not-allowed', 'Not allowed')); + } + } + setUserAvatar(user, Buffer.concat(imageData), mimetype, 'rest'); + callback(); + })); + })); + busboy.on('field', (fieldname, val) => { + fields[fieldname] = val; + }); + this.request.pipe(busboy); + })(); + } + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('users.getStatus', { authRequired: true }, { + get() { + if (this.isUserFromParams()) { + const user = Users.findOneById(this.userId); + return API.v1.success({ + message: user.statusText, + connectionStatus: user.statusConnection, + status: user.status, + }); + } + + const user = this.getUserFromParams(); + + return API.v1.success({ + message: user.statusText, + status: user.status, + }); + }, +}); + +API.v1.addRoute('users.setStatus', { authRequired: true }, { + post() { + check(this.bodyParams, Match.ObjectIncluding({ + status: Match.Maybe(String), + message: Match.Maybe(String), + })); + + if (!settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { + method: 'users.setStatus', + }); + } + + let user; + if (this.isUserFromParams()) { + user = Meteor.users.findOne(this.userId); + } else if (hasPermission(this.userId, 'edit-other-user-info')) { + user = this.getUserFromParams(); + } else { + return API.v1.unauthorized(); + } + + Meteor.runAsUser(user._id, () => { + if (this.bodyParams.message || this.bodyParams.message.length === 0) { + setStatusText(user._id, this.bodyParams.message); + } + if (this.bodyParams.status) { + const validStatus = ['online', 'away', 'offline', 'busy']; + if (validStatus.includes(this.bodyParams.status)) { + Meteor.users.update(this.userId, { + $set: { + status: this.bodyParams.status, + statusDefault: this.bodyParams.status, + }, + }); + } else { + throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { + method: 'users.setStatus', + }); + } + } + }); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('users.update', { authRequired: true }, { + post() { + check(this.bodyParams, { + userId: String, + data: Match.ObjectIncluding({ + email: Match.Maybe(String), + name: Match.Maybe(String), + password: Match.Maybe(String), + username: Match.Maybe(String), + statusText: Match.Maybe(String), + active: Match.Maybe(Boolean), + roles: Match.Maybe(Array), + joinDefaultChannels: Match.Maybe(Boolean), + requirePasswordChange: Match.Maybe(Boolean), + sendWelcomeEmail: Match.Maybe(Boolean), + verified: Match.Maybe(Boolean), + customFields: Match.Maybe(Object), + }), + }); + + const userData = _.extend({ _id: this.bodyParams.userId }, this.bodyParams.data); + + Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData)); + + if (this.bodyParams.data.customFields) { + saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); + } + + if (typeof this.bodyParams.data.active !== 'undefined') { + Meteor.runAsUser(this.userId, () => { + Meteor.call('setUserActiveStatus', this.bodyParams.userId, this.bodyParams.data.active); + }); + } + + return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields: API.v1.defaultFieldsToExclude }) }); + }, +}); + +API.v1.addRoute('users.updateOwnBasicInfo', { authRequired: true }, { + post() { + check(this.bodyParams, { + data: Match.ObjectIncluding({ + email: Match.Maybe(String), + name: Match.Maybe(String), + username: Match.Maybe(String), + statusText: Match.Maybe(String), + currentPassword: Match.Maybe(String), + newPassword: Match.Maybe(String), + }), + customFields: Match.Maybe(Object), + }); + + const userData = { + email: this.bodyParams.data.email, + realname: this.bodyParams.data.name, + username: this.bodyParams.data.username, + statusText: this.bodyParams.data.statusText, + newPassword: this.bodyParams.data.newPassword, + typedPassword: this.bodyParams.data.currentPassword, + }; + + Meteor.runAsUser(this.userId, () => Meteor.call('saveUserProfile', userData, this.bodyParams.customFields)); + + return API.v1.success({ user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }) }); + }, +}); + +API.v1.addRoute('users.createToken', { authRequired: true }, { + post() { + const user = this.getUserFromParams(); + let data; + Meteor.runAsUser(this.userId, () => { + data = Meteor.call('createToken', user._id); + }); + return data ? API.v1.success({ data }) : API.v1.unauthorized(); + }, +}); + +API.v1.addRoute('users.getPreferences', { authRequired: true }, { + get() { + const user = Users.findOneById(this.userId); + if (user.settings) { + const { preferences = {} } = user.settings; + preferences.language = user.language; + + return API.v1.success({ + preferences, + }); + } + return API.v1.failure(TAPi18n.__('Accounts_Default_User_Preferences_not_available').toUpperCase()); + }, +}); + +API.v1.addRoute('users.setPreferences', { authRequired: true }, { + post() { + check(this.bodyParams, { + userId: Match.Maybe(String), + data: Match.ObjectIncluding({ + newRoomNotification: Match.Maybe(String), + newMessageNotification: Match.Maybe(String), + clockMode: Match.Maybe(Number), + useEmojis: Match.Maybe(Boolean), + convertAsciiEmoji: Match.Maybe(Boolean), + saveMobileBandwidth: Match.Maybe(Boolean), + collapseMediaByDefault: Match.Maybe(Boolean), + autoImageLoad: Match.Maybe(Boolean), + emailNotificationMode: Match.Maybe(String), + unreadAlert: Match.Maybe(Boolean), + notificationsSoundVolume: Match.Maybe(Number), + desktopNotifications: Match.Maybe(String), + mobileNotifications: Match.Maybe(String), + enableAutoAway: Match.Maybe(Boolean), + highlights: Match.Maybe(Array), + desktopNotificationDuration: Match.Maybe(Number), + messageViewMode: Match.Maybe(Number), + hideUsernames: Match.Maybe(Boolean), + hideRoles: Match.Maybe(Boolean), + hideAvatars: Match.Maybe(Boolean), + hideFlexTab: Match.Maybe(Boolean), + sendOnEnter: Match.Maybe(String), + roomCounterSidebar: Match.Maybe(Boolean), + language: Match.Maybe(String), + sidebarShowFavorites: Match.Optional(Boolean), + sidebarShowUnread: Match.Optional(Boolean), + sidebarSortby: Match.Optional(String), + sidebarViewMode: Match.Optional(String), + sidebarHideAvatar: Match.Optional(Boolean), + sidebarGroupByType: Match.Optional(Boolean), + sidebarShowDiscussion: Match.Optional(Boolean), + muteFocusedConversations: Match.Optional(Boolean), + }), + }); + const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId; + const userData = { + _id: userId, + settings: { + preferences: this.bodyParams.data, + }, + }; + + if (this.bodyParams.data.language) { + const { language } = this.bodyParams.data; + delete this.bodyParams.data.language; + userData.language = language; + } + + Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData)); + const user = Users.findOneById(userId, { + fields: { + 'settings.preferences': 1, + language: 1, + }, + }); + + return API.v1.success({ + user: { + _id: user._id, + settings: { + preferences: { + ...user.settings.preferences, + language: user.language, + }, + }, + }, + }); + }, +}); + +API.v1.addRoute('users.forgotPassword', { authRequired: false }, { + post() { + const { email } = this.bodyParams; + if (!email) { + return API.v1.failure('The \'email\' param is required'); + } + + const emailSent = Meteor.call('sendForgotPasswordEmail', email); + if (emailSent) { + return API.v1.success(); + } + return API.v1.failure('User not found'); + }, +}); + +API.v1.addRoute('users.getUsernameSuggestion', { authRequired: true }, { + get() { + const result = Meteor.runAsUser(this.userId, () => Meteor.call('getUsernameSuggestion')); + + return API.v1.success({ result }); + }, +}); + +API.v1.addRoute('users.generatePersonalAccessToken', { authRequired: true }, { + post() { + const { tokenName } = this.bodyParams; + if (!tokenName) { + return API.v1.failure('The \'tokenName\' param is required'); + } + const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName })); + + return API.v1.success({ token }); + }, +}); + +API.v1.addRoute('users.regeneratePersonalAccessToken', { authRequired: true }, { + post() { + const { tokenName } = this.bodyParams; + if (!tokenName) { + return API.v1.failure('The \'tokenName\' param is required'); + } + const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:regenerateToken', { tokenName })); + + return API.v1.success({ token }); + }, +}); + +API.v1.addRoute('users.getPersonalAccessTokens', { authRequired: true }, { + get() { + if (!hasPermission(this.userId, 'create-personal-access-tokens')) { + throw new Meteor.Error('not-authorized', 'Not Authorized'); + } + const loginTokens = Users.getLoginTokensByUserId(this.userId).fetch()[0]; + const getPersonalAccessTokens = () => loginTokens.services.resume.loginTokens + .filter((loginToken) => loginToken.type && loginToken.type === 'personalAccessToken') + .map((loginToken) => ({ + name: loginToken.name, + createdAt: loginToken.createdAt, + lastTokenPart: loginToken.lastTokenPart, + })); + + return API.v1.success({ + tokens: loginTokens ? getPersonalAccessTokens() : [], + }); + }, +}); + +API.v1.addRoute('users.removePersonalAccessToken', { authRequired: true }, { + post() { + const { tokenName } = this.bodyParams; + if (!tokenName) { + return API.v1.failure('The \'tokenName\' param is required'); + } + Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:removeToken', { + tokenName, + })); + + return API.v1.success(); + }, +}); + +API.v1.addRoute('users.presence', { authRequired: true }, { + get() { + const { from } = this.queryParams; + + const options = { + fields: { + username: 1, + name: 1, + status: 1, + utcOffset: 1, + statusText: 1, + }, + }; + + if (from) { + const ts = new Date(from); + const diff = (Date.now() - ts) / 1000 / 60; + + if (diff < 10) { + return API.v1.success({ + users: Users.findNotIdUpdatedFrom(this.userId, ts, options).fetch(), + full: false, + }); + } + } + + return API.v1.success({ + users: Users.findUsersNotOffline(options).fetch(), + full: true, + }); + }, +}); diff --git a/app/api/server/v1/video-conference.js b/app/api/server/v1/video-conference.js new file mode 100644 index 000000000000..51d0b8830499 --- /dev/null +++ b/app/api/server/v1/video-conference.js @@ -0,0 +1,22 @@ +import { Meteor } from 'meteor/meteor'; + +import { Rooms } from '../../../models'; +import { API } from '../api'; + +API.v1.addRoute('video-conference/jitsi.update-timeout', { authRequired: true }, { + post() { + const { roomId } = this.bodyParams; + if (!roomId) { + return API.v1.failure('The "roomId" parameter is required!'); + } + + const room = Rooms.findOneById(roomId); + if (!room) { + return API.v1.failure('Room does not exist!'); + } + + Meteor.runAsUser(this.userId, () => Meteor.call('jitsi:updateTimeout', roomId)); + + return API.v1.success({ jitsiTimeout: Rooms.findOneById(roomId).jitsiTimeout }); + }, +}); diff --git a/packages/rocketchat-apps/.gitignore b/app/apps/.gitignore similarity index 100% rename from packages/rocketchat-apps/.gitignore rename to app/apps/.gitignore diff --git a/packages/rocketchat-apps/README.md b/app/apps/README.md similarity index 100% rename from packages/rocketchat-apps/README.md rename to app/apps/README.md diff --git a/app/apps/assets/stylesheets/apps.css b/app/apps/assets/stylesheets/apps.css new file mode 100644 index 000000000000..438b99d813ee --- /dev/null +++ b/app/apps/assets/stylesheets/apps.css @@ -0,0 +1,381 @@ +.rc-apps-section, +.rc-apps-marketplace { + display: flex; + + overflow: auto; + flex-direction: column; + + height: 100vh; + + padding: 1.25rem 2rem; + + font-size: 14px; + + &.page-settings .rc-apps-container { + a { + color: var(--rc-color-button-primary); + + font-weight: 500; + } + } + + h1 { + margin-bottom: 0; + + letter-spacing: 0; + text-transform: initial; + + color: var(--color-dark-medium); + + font-size: 22px; + font-weight: normal; + line-height: 28px; + } + + h2 { + margin-top: 10px; + margin-bottom: 0; + + letter-spacing: 0; + text-transform: initial; + + color: var(--color-dark); + + font-size: 16px; + font-weight: 500; + line-height: 24px; + } + + h3 { + margin-bottom: 0; + + text-align: left; + letter-spacing: 0; + text-transform: initial; + + color: var(--color-gray); + + font-size: 14px; + font-weight: 500; + line-height: 20px; + } + + .rc-apps-container { + margin-top: 0; + padding-bottom: 15px; + } + + .rc-apps-container__header { + padding-top: 10px; + + border-bottom: 1.5px solid var(--color-gray-lightest); + } + + /* + .js-install { + margin-top: 6px; + } */ + + .content { + /* display: block !important; */ + padding: 0 !important; + + > .rc-apps-container { + display: block; + overflow-y: scroll; + + padding: 0 !important; + } + + > .rc-apps-details { + display: block; + } + } + + .rc-apps-category { + margin-right: 8px; + padding: 8px; + + text-align: left; + letter-spacing: -0.17px; + text-transform: uppercase; + + color: #9da2a9; + border-radius: 2px; + background: var(--color-gray-lightest); + + font-size: 12px; + font-weight: 500; + } + + .app-enable-loading .loading-animation { + margin-left: 50px; + justify-content: left; + } + + .apps-error { + display: flex; + flex-direction: column; + + width: 100%; + height: calc(100% - 60px); + padding: 25px 40px; + + font-size: 45px; + align-items: center; + justify-content: center; + } + + .rc-table-avatar { + width: 40px; + height: 40px; + margin: 0 7px; + } + + .rc-table-info { + height: 40px; + margin: 0 7px; + } + + .rc-app-price { + position: relative; + top: -3px; + } + + .rc-table-td--medium { + width: 300px; + } + + .rc-table td { + padding: 0.5rem 0; + + padding-right: 10px; + } + + &__wrap-actions { + & > .loading { + display: none; + } + + &.loading { + & > .loading { + display: block; + + font-size: 11px; + font-weight: 600; + + & > .rc-icon--loading { + animation: spin 1s linear infinite; + } + } + + & > .apps-installer { + display: none; + } + } + } + + .arrow-up { + transform: rotate(180deg); + } + + &.page-settings .content .rocket-form .section { + padding: 0 2.5em; + + border-bottom: none; + + &:hover { + background-color: var(--rc-color-primary-lightest); + } + } + + .rc-table-content { + display: flex; + overflow-x: auto; + flex-direction: column; + flex: 1 1 100%; + + height: 100vh; + + margin-top: 20px; + + & .rc-form-filters { + margin: 8px 0; + } + + & .js-sort { + cursor: pointer; + + &.is-sorting .table-fake-th .rc-icon { + opacity: 1; + } + } + + & .table-fake-th { + &:hover .rc-icon { + opacity: 1; + } + + & .rc-icon { + transition: opacity 0.3s; + + opacity: 0; + + font-size: 1rem; + } + } + + & tbody .rc-table-tr .rc-apps-section__app-menu-trigger { + visibility: hidden; + } + + & tbody .rc-table-tr:hover .rc-apps-section__app-menu-trigger { + visibility: visible; + } + + & tbody .rc-table-tr:not(.table-no-click):not(.table-no-pointer):hover { + background-color: #f7f8fa; + } + + & .rc-table-info { + margin: 0; + justify-content: center; + + & .rc-table-title, + & .rc-table-subtitle { + font-size: 0.875rem; + line-height: 1.25rem; + } + + & .rc-apps-categories { + display: flex; + + height: 1.25rem; + margin: 0 -0.25rem; + align-items: center; + flex-wrap: wrap; + + & .rc-apps-category { + overflow: hidden; + flex: 0 0 auto; + + box-sizing: border-box; + margin: 0.125rem 0.25rem; + padding: 0.0625rem 0.25rem; + + text-transform: none; + text-overflow: ellipsis; + + color: var(--color-gray); + background-color: var(--color-gray-lightest); + + font-size: 0.625rem; + font-weight: 500; + line-height: 0.875rem; + } + } + } + } + + @media (width <= 700px) { + .rc-table-content { + & th:not(:first-child), + & td:not(:first-child) { + display: none; + } + } + } + + &__app-menu-trigger { + + position: relative; + + display: flex; + flex: 0 0 auto; + + margin-left: auto; + padding: 0; + + font-size: 0.875rem; + line-height: 1.25rem; + align-items: center; + appearance: none; + margin-inline-start: auto; + + &:active { + transform: translateY(2px); + + opacity: 0.9; + } + + &:active::before { + top: -2px; + } + + &::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + + content: ""; + cursor: pointer; + } + + & .rc-icon { + margin: 0; + } + } + + &__spinning-icon { + animation: spin 1s linear infinite; + } + + &__button--working { + opacity: 0.6; + } + + &__status { + width: 100%; + + color: var(--rc-color-primary-light); + + line-height: 40px; + + &--warning { + color: var(--rc-color-alert); + } + + &--failed { + color: var(--rc-color-error); + } + } + + &__status-column { + width: 150px; + } + + tr .rc-apps-section__table-button--hideable { + visibility: hidden; + } + + tr .rc-apps-section__table-button--working, + tr:hover .rc-apps-section__table-button--hideable { + visibility: visible; + } + + .rc-apps-section__table-button--working { + opacity: 0.6; + } +} + +@keyframes play90 { + 0% { + right: -798px; + } + + 100% { + right: 2px; + } +} diff --git a/app/apps/client/admin/appInstall.html b/app/apps/client/admin/appInstall.html new file mode 100644 index 000000000000..86fffb019c87 --- /dev/null +++ b/app/apps/client/admin/appInstall.html @@ -0,0 +1,66 @@ + diff --git a/packages/rocketchat-apps/client/admin/appInstall.js b/app/apps/client/admin/appInstall.js similarity index 83% rename from packages/rocketchat-apps/client/admin/appInstall.js rename to app/apps/client/admin/appInstall.js index 498410a3de3c..8ff298c54feb 100644 --- a/packages/rocketchat-apps/client/admin/appInstall.js +++ b/app/apps/client/admin/appInstall.js @@ -7,6 +7,13 @@ // if you're developing it and using a rest api with a particular parameter passed // then it will be enabled by default for development reasons. The server prefers a url // over the passed in body, so if both are found it will only use the url. +import { ReactiveVar } from 'meteor/reactive-var'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Template } from 'meteor/templating'; +import { Tracker } from 'meteor/tracker'; + +import { APIClient } from '../../../utils'; +import { SideNav } from '../../../ui-utils/client'; Template.appInstall.helpers({ appFile() { @@ -69,9 +76,9 @@ Template.appInstall.events({ let result; if (isUpdating) { - result = await RocketChat.API.post(`apps/${ t.isUpdatingId.get() }`, { url }); + result = await APIClient.post(`apps/${ t.isUpdatingId.get() }`, { url }); } else { - result = await RocketChat.API.post('apps', { url }); + result = await APIClient.post('apps', { url }); } if (result.compilerErrors.length !== 0 || result.app.status === 'compiler_error') { @@ -112,9 +119,9 @@ Template.appInstall.events({ let result; if (isUpdating) { - result = await RocketChat.API.upload(`apps/${ t.isUpdatingId.get() }`, data); + result = await APIClient.upload(`apps/${ t.isUpdatingId.get() }`, data); } else { - result = await RocketChat.API.upload('apps', data); + result = await APIClient.upload('apps', data); } console.log('install result', result); @@ -131,3 +138,10 @@ Template.appInstall.events({ t.isInstalling.set(false); }, }); + +Template.appInstall.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); diff --git a/app/apps/client/admin/appLogs.html b/app/apps/client/admin/appLogs.html new file mode 100644 index 000000000000..47e901331c89 --- /dev/null +++ b/app/apps/client/admin/appLogs.html @@ -0,0 +1,55 @@ + diff --git a/app/apps/client/admin/appLogs.js b/app/apps/client/admin/appLogs.js new file mode 100644 index 000000000000..46e82534c05b --- /dev/null +++ b/app/apps/client/admin/appLogs.js @@ -0,0 +1,115 @@ +import { ReactiveVar } from 'meteor/reactive-var'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Template } from 'meteor/templating'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { Tracker } from 'meteor/tracker'; +import moment from 'moment'; +import hljs from 'highlight.js'; + +import { APIClient } from '../../../utils'; +import { SideNav } from '../../../ui-utils/client'; + +const loadData = (instance) => { + Promise.all([ + APIClient.get(`apps/${ instance.id.get() }`), + APIClient.get(`apps/${ instance.id.get() }/logs`), + ]).then((results) => { + instance.app.set(results[0].app); + instance.logs.set(results[1].logs); + + instance.ready.set(true); + }).catch((e) => { + instance.hasError.set(true); + instance.theError.set(e.message); + }); +}; + +Template.appLogs.onCreated(function() { + const instance = this; + this.id = new ReactiveVar(FlowRouter.getParam('appId')); + this.ready = new ReactiveVar(false); + this.hasError = new ReactiveVar(false); + this.theError = new ReactiveVar(''); + this.app = new ReactiveVar({}); + this.logs = new ReactiveVar([]); + + loadData(instance); +}); + +Template.appLogs.helpers({ + isReady() { + if (Template.instance().ready) { + return Template.instance().ready.get(); + } + + return false; + }, + hasError() { + if (Template.instance().hasError) { + return Template.instance().hasError.get(); + } + + return false; + }, + theError() { + if (Template.instance().theError) { + return Template.instance().theError.get(); + } + + return ''; + }, + app() { + return Template.instance().app.get(); + }, + logs() { + return Template.instance().logs.get(); + }, + formatDate(date) { + return moment(date).format('L LTS'); + }, + jsonStringify(data) { + let value = ''; + + if (!data) { + return value; + } if (typeof data === 'object') { + value = hljs.highlight('json', JSON.stringify(data, null, 2)).value; + } else { + value = hljs.highlight('json', data).value; + } + + return value.replace(/\\\\n/g, '
'); + }, + title() { + return TAPi18n.__('View_the_Logs_for', { name: Template.instance().app.get().name }); + }, +}); + +Template.appLogs.events({ + 'click .section-collapsed .section-title': (e) => { + $(e.currentTarget).closest('.section').removeClass('section-collapsed').addClass('section-expanded'); + $(e.currentTarget).find('.button-down').addClass('arrow-up'); + }, + + 'click .section-expanded .section-title': (e) => { + $(e.currentTarget).closest('.section').removeClass('section-expanded').addClass('section-collapsed'); + $(e.currentTarget).find('.button-down').removeClass('arrow-up'); + }, + + 'click .js-cancel': () => { + FlowRouter.go('apps'); + }, + + 'click .js-refresh': (e, t) => { + t.ready.set(false); + t.logs.set([]); + loadData(t); + }, +}); + +Template.appLogs.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); diff --git a/app/apps/client/admin/appManage.css b/app/apps/client/admin/appManage.css new file mode 100644 index 000000000000..caeba3c95d6c --- /dev/null +++ b/app/apps/client/admin/appManage.css @@ -0,0 +1,181 @@ +#rocket-chat .rc-apps-details { + margin-bottom: 0; + padding: 0; + + &__photo { + width: 96px; + height: 96px; + margin-right: 21px; + } + + &__content { + padding: 0; + + color: var(--color-gray); + justify-content: flex-start; + } + + &__description { + padding-bottom: 50px; + + border-bottom: 1.5px solid var(--color-gray-light); + } + + &__col { + display: inline-block; + + margin-right: 8px; + } + + &__bundles { + display: flex; + + padding-bottom: 20px; + + border-bottom: 1.5px solid var(--color-gray-light); + } + + &__bundle { + display: flex; + + width: 50%; + } + + &__bundle_icons { + display: flex; + overflow: hidden; + + min-width: 99px; + max-width: 99px; + height: 99px; + + padding: 2px; + + border-radius: 2px; + + background-color: var(--color-gray-light); + flex-wrap: wrap; + } + + &__bundle_icon { + min-width: 40px; + max-width: 40px; + height: 40px; + + margin-top: 5px; + margin-left: 5px; + + border-radius: 2px; + + background-color: #f7f7f7; + background-repeat: no-repeat; + background-position: center center; + background-size: contain; + } + + &__bundle_body { + padding: 5px 10px; + + color: var(--color-gray); + + &_title { + color: var(--color-darkest); + + font-size: 1.1em; + font-weight: 500; + } + } + + &__alert { + margin: 0.25rem 0; + padding: 0.5rem 1rem; + + border-radius: 4px; + + font-size: 0.875rem; + line-height: 1.25rem; + } + + &__alert-error { + color: var(--color-red); + background-color: #ffe9ec; + } + + &__alert-warning { + color: #b68d00; + background-color: #fff6d6; + } + + &__name { + flex: 0 0 1.75rem; + + margin: 0; + + text-transform: none; + + color: var(--color-dark-medium); + + font-family: inherit; + font-size: 1.375rem; + font-weight: normal; + line-height: 1.75rem; + } + + &__author { + font-family: inherit; + font-size: 14px; + font-weight: 500; + line-height: 20px; + } + + &__side-info { + display: inline-flex; + align-items: center; + + &::before { + display: inline-block; + + width: 1px; + height: 12px; + margin: 0 8px; + + content: ''; + + background-color: currentColor; + } + + &--twice::before { + margin: 0 16px; + } + } + + &__side-info-wrapper { + flex: 1; + } + + &__row--centered { + align-items: center; + } + + &__app-status { + display: flex; + flex: 1; + + margin-top: 8px; + align-items: center; + } + + & .rc-button.loading { + padding: 0 1.5rem; + + opacity: 0.6; + + &::before { + display: none; + } + + & > .rc-icon { + animation: spin 1s linear infinite; + } + } +} diff --git a/app/apps/client/admin/appManage.html b/app/apps/client/admin/appManage.html new file mode 100644 index 000000000000..037f19cfca0e --- /dev/null +++ b/app/apps/client/admin/appManage.html @@ -0,0 +1,522 @@ + diff --git a/app/apps/client/admin/appManage.js b/app/apps/client/admin/appManage.js new file mode 100644 index 000000000000..233acd09e4aa --- /dev/null +++ b/app/apps/client/admin/appManage.js @@ -0,0 +1,501 @@ +import { Meteor } from 'meteor/meteor'; +import { ReactiveDict } from 'meteor/reactive-dict'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Template } from 'meteor/templating'; +import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n'; +import { Tracker } from 'meteor/tracker'; +import _ from 'underscore'; + +import { SideNav } from '../../../ui-utils/client'; +import { isEmail } from '../../../utils'; +import { Utilities } from '../../lib/misc/Utilities'; +import { AppEvents } from '../communication'; +import { Apps } from '../orchestrator'; +import { + appButtonProps, + appStatusSpanProps, + formatPrice, + formatPricingPlan, + handleAPIError, + triggerAppPopoverMenu, + promptSubscription, + warnStatusChange, + checkCloudLogin, +} from './helpers'; + +import './appManage.html'; +import './appManage.css'; + + +const attachAPIs = async (appId, state) => { + try { + const apis = await Apps.getAppApis(appId); + state.set('apis', apis); + } catch (error) { + handleAPIError(error); + } +}; + +const attachSettings = async (appId, state) => { + try { + const settings = await Apps.getAppSettings(appId); + + for (const setting of Object.values(settings)) { + setting.i18nPlaceholder = setting.i18nPlaceholder || ' '; + setting.value = setting.value !== undefined && setting.value !== null ? setting.value : setting.packageValue; + setting.oldValue = setting.value; + setting.hasChanged = false; + } + + state.set('settings', settings); + } catch (error) { + handleAPIError(error); + } +}; + +const attachBundlesApps = (bundledIn, _app) => { + if (!bundledIn || !bundledIn.length) { + return; + } + + bundledIn.forEach(async (bundle, i) => { + try { + const apps = await Apps.getAppsOnBundle(bundle.bundleId); + bundle.apps = apps.slice(0, 4); + } catch (error) { + handleAPIError(error); + } + + bundledIn[i] = bundle; + _app.set('bundledIn', bundledIn); + }); +}; + +const attachMarketplaceInformation = async (appId, version, _app) => { + try { + const { + categories, + isPurchased, + price, + bundledIn, + purchaseType, + subscriptionInfo, + version: marketplaceVersion, + } = await Apps.getLatestAppFromMarketplace(appId, version); + + _app.set({ + categories, + isPurchased, + price, + bundledIn, + purchaseType, + subscriptionInfo, + marketplaceVersion, + }); + + attachBundlesApps(bundledIn, _app); + } catch (error) { + if (error.xhr && error.xhr.status === 404) { + return; + } + + handleAPIError(error); + } +}; + +const loadApp = async ({ appId, version, state, _app }) => { + let app; + try { + app = await Apps.getApp(appId); + } catch (error) { + console.error(error); + } + + state.set('settings', {}); + + if (app) { + state.set('isLoading', false); + _app.clear(); + _app.set({ ...app, installed: true }); + + attachAPIs(appId, state); + attachSettings(appId, state); + attachMarketplaceInformation(appId, version, _app); + + if (FlowRouter.current().route.getRouteName() === 'marketplace-app') { + FlowRouter.withReplaceState(() => { + FlowRouter.go('app-manage', { appId }); + }); + return; + } + + return; + } + + try { + app = await Apps.getAppFromMarketplace(appId, version); + } catch (error) { + state.set('error', error); + } + + if (app) { + delete app.status; + app.marketplaceVersion = app.version; + state.set('isLoading', false); + _app.clear(); + _app.set({ ...app, installed: false }); + + attachBundlesApps(app.bundledIn, _app); + + if (FlowRouter.current().route.getRouteName() === 'app-manage') { + FlowRouter.withReplaceState(() => { + FlowRouter.go('marketplace-app', { appId }); + }); + } + } +}; + +Template.appManage.onCreated(function() { + this.appId = FlowRouter.getParam('appId'); + this.version = FlowRouter.getQueryParam('version'); + this.state = new ReactiveDict({ + settings: {}, + isLoading: true, + isSaving: false, + }); + this._app = new ReactiveDict({ + id: this.appId, + version: this.version, + }); + + loadApp(this); + + this.__ = (key, options, lang_tag) => { + const appKey = Utilities.getI18nKeyForApp(key, this.appId); + return TAPi18next.exists(`project:${ appKey }`) + ? TAPi18n.__(appKey, options, lang_tag) + : TAPi18n.__(key, options, lang_tag); + }; + + const withAppIdFilter = (f) => (maybeAppId, ...args) => { + const appId = maybeAppId.appId || maybeAppId; + if (appId !== this.appId) { + return; + } + + f.call(this, maybeAppId, ...args); + }; + + this.handleSettingUpdated = withAppIdFilter(() => { + attachSettings(this.appId, this.state); + }); + + this.handleChange = withAppIdFilter(() => { + loadApp(this); + }); + + Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleChange); + Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleChange); + Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleChange); + Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleChange); + Apps.getWsListener().registerListener(AppEvents.APP_SETTING_UPDATED, this.handleSettingUpdated); +}); + +Template.apps.onDestroyed(function() { + Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleChange); + Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleChange); + Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleChange); + Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleChange); + Apps.getWsListener().unregisterListener(AppEvents.APP_SETTING_UPDATED, this.handleSettingUpdated); +}); + +Template.appManage.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); + +Template.appManage.helpers({ + isSettingsPristine() { + const settings = Template.instance().state.get('settings'); + return !Object.values(settings).some(({ hasChanged }) => hasChanged); + }, + isSaving() { + return Template.instance().state.get('isSaving'); + }, + error() { + const error = Template.instance().state.get('error'); + + return error && ( + (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) + || error.message + ); + }, + isLoading() { + return Template.instance().state.get('isLoading'); + }, + appButtonProps, + appStatusSpanProps, + priceDisplay() { + const [purchaseType, price, pricingPlans] = [ + Template.instance()._app.get('purchaseType'), + Template.instance()._app.get('price'), + Template.instance()._app.get('pricingPlans'), + ]; + if (purchaseType === 'subscription') { + if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) { + return; + } + + return formatPricingPlan(pricingPlans[0]); + } + + if (price > 0) { + return formatPrice(price); + } + + return 'Free'; + }, + isEmail, + _(key, ...args) { + const [i18nArgs, keyword] = [args.slice(-2), args.slice(-1)[0]]; + + return Template.instance().__(key, { + ...keyword.hash, + sprintf: i18nArgs, + }); + }, + languages() { + return [ + { + key: '', + name: 'Default', + en: 'Default', + }, + ...Object.entries(TAPi18n.getLanguages()) + .map(([key, language]) => ({ key, ...language })) + .sort(({ key: a }, { key: b }) => a.localeCompare(b)), + ]; + }, + selectedOption(_id, val) { + const settings = Template.instance().state.get('settings'); + return settings[_id].value === val; + }, + app() { + return Template.instance()._app.all(); + }, + errors() { + const { errors = {} } = Template.instance()._app.get('licenseValidation') || {}; + return Object.values(errors); + }, + warnings() { + const { warnings = {} } = Template.instance()._app.get('licenseValidation') || {}; + return Object.values(warnings); + }, + settings() { + return Object.values(Template.instance().state.get('settings')); + }, + apis() { + return Template.instance().state.get('apis'); + }, + curl(method, api) { + const example = api.examples[method] || {}; + return Utilities.curl({ + url: Meteor.absoluteUrl.defaultOptions.rootUrl + api.computedPath, + method, + params: example.params, + query: example.query, + content: example.content, + headers: example.headers, + }).split('\n'); + }, + renderMethods(methods) { + return methods.join('|').toUpperCase(); + }, + bundleAppNames(apps) { + return apps.map((app) => app.latest.name).join(', '); + }, +}); + +Template.appManage.events({ + 'click .js-cancel-editing-settings'(event, instance) { + const settings = instance.state.get('settings'); + + for (const setting of Object.values(settings)) { + setting.value = setting.oldValue; + setting.hasChanged = false; + } + + instance.state.set('settings', settings); + }, + + async 'click .js-save-settings'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + const { appId, state } = instance; + + if (state.get('isSaving')) { + return; + } + + state.set('isSaving', true); + + const settings = state.get('settings'); + + try { + const toSave = Object.values(settings) + .filter(({ hasChanged }) => hasChanged); + + if (!toSave.length) { + return; + } + + const updated = await Apps.setAppSettings(appId, toSave); + updated.forEach(({ id, value }) => { + settings[id].value = value; + settings[id].oldValue = value; + settings[id].hasChanged = false; + }); + + state.set('settings', settings); + } catch (error) { + handleAPIError(error); + } finally { + state.set('isSaving', false); + } + }, + 'click .js-close'() { + if (FlowRouter.current().route.getRouteName() === 'marketplace-app') { + FlowRouter.go('marketplace'); + return; + } + + if (FlowRouter.current().route.getRouteName() === 'app-manage') { + FlowRouter.go('apps'); + return; + } + + window.history.back(); + }, + 'click .js-menu'(event, instance) { + event.stopPropagation(); + const { currentTarget } = event; + + triggerAppPopoverMenu(instance._app.all(), currentTarget, instance); + }, + + async 'click .js-install, click .js-update'(event, instance) { + event.stopPropagation(); + + if (!await checkCloudLogin()) { + return; + } + + const { appId, _app } = instance; + + _app.set('working', true); + + try { + const { status } = await Apps.installApp(appId, _app.get('marketplaceVersion')); + warnStatusChange(_app.get('name'), status); + } catch (error) { + handleAPIError(error); + } finally { + _app.set('working', false); + } + }, + + async 'click .js-purchase'(event, instance) { + const { _app } = instance; + + if (!await checkCloudLogin()) { + return; + } + + _app.set('working', true); + + const app = _app.all(); + + await promptSubscription(app, async () => { + try { + const { status } = await Apps.installApp(app.id, app.marketplaceVersion); + warnStatusChange(app.name, status); + } catch (error) { + handleAPIError(error); + } finally { + _app.set('working', false); + } + }, () => _app.set('working', false)); + }, + + 'change input[type="checkbox"]'(event, instance) { + const { id } = this; + const { state } = instance; + + const settings = state.get('settings'); + const setting = settings[id]; + + if (!setting) { + return; + } + + const value = event.currentTarget.checked; + + setting.value = value; + setting.hasChanged = setting.oldValue !== setting.value; + + state.set('settings', settings); + }, + + 'change .rc-select__element'(event, instance) { + const { id } = this; + const { state } = instance; + + const settings = state.get('settings'); + const setting = settings[id]; + + if (!setting) { + return; + } + + const { value } = event.currentTarget; + + setting.value = value; + setting.hasChanged = setting.oldValue !== setting.value; + + state.set('settings', settings); + }, + + 'input input, input textarea, change input[type="color"]': _.throttle(function(event, instance) { + const { type, id } = this; + const { state } = instance; + + const settings = state.get('settings'); + const setting = settings[id]; + + if (!setting) { + return; + } + + let value = event.currentTarget.value.trim(); + + switch (type) { + case 'int': + value = parseInt(value); + break; + case 'boolean': + value = value === '1'; + break; + case 'code': + value = $(`.code-mirror-box[data-editor-id="${ id }"] .CodeMirror`)[0].CodeMirror.getValue(); + break; + } + + setting.value = value; + setting.hasChanged = setting.oldValue !== setting.value; + + state.set('settings', settings); + }, 500), +}); diff --git a/packages/rocketchat-apps/client/admin/appWhatIsIt.html b/app/apps/client/admin/appWhatIsIt.html similarity index 100% rename from packages/rocketchat-apps/client/admin/appWhatIsIt.html rename to app/apps/client/admin/appWhatIsIt.html diff --git a/app/apps/client/admin/appWhatIsIt.js b/app/apps/client/admin/appWhatIsIt.js new file mode 100644 index 000000000000..dd63716de0f5 --- /dev/null +++ b/app/apps/client/admin/appWhatIsIt.js @@ -0,0 +1,55 @@ +import { Meteor } from 'meteor/meteor'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Template } from 'meteor/templating'; +import { Tracker } from 'meteor/tracker'; + +import { Apps } from '../orchestrator'; +import { SideNav } from '../../../ui-utils/client'; + +Template.appWhatIsIt.onCreated(function() { + this.isLoading = new ReactiveVar(false); + this.hasError = new ReactiveVar(false); +}); + +Template.appWhatIsIt.helpers({ + isLoading() { + if (Template.instance().isLoading) { + return Template.instance().isLoading.get(); + } + + return false; + }, + hasError() { + if (Template.instance().hasError) { + return Template.instance().hasError.get(); + } + + return false; + }, +}); + +Template.appWhatIsIt.events({ + 'click .js-enable'(e, t) { + t.isLoading.set(true); + + Meteor.call('apps/go-enable', function _appsMightHaveBeenEnabled(error) { + if (error) { + t.hasError.set(true); + t.isLoading.set(false); + return; + } + + Apps.load(true); + + FlowRouter.go('/admin/apps'); + }); + }, +}); + +Template.appWhatIsIt.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); diff --git a/app/apps/client/admin/apps.html b/app/apps/client/admin/apps.html new file mode 100644 index 000000000000..f0d514a6855f --- /dev/null +++ b/app/apps/client/admin/apps.html @@ -0,0 +1,128 @@ + diff --git a/app/apps/client/admin/apps.js b/app/apps/client/admin/apps.js new file mode 100644 index 000000000000..b98621d496bf --- /dev/null +++ b/app/apps/client/admin/apps.js @@ -0,0 +1,284 @@ +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { ReactiveDict } from 'meteor/reactive-dict'; +import { Template } from 'meteor/templating'; +import { Tracker } from 'meteor/tracker'; + +import { settings } from '../../../settings'; +import { AppEvents } from '../communication'; +import { Apps } from '../orchestrator'; +import { SideNav } from '../../../ui-utils/client'; +import { + appButtonProps, + appStatusSpanProps, + checkCloudLogin, + handleAPIError, + promptSubscription, + triggerAppPopoverMenu, + warnStatusChange, +} from './helpers'; + +import './apps.html'; + + +Template.apps.onCreated(function() { + this.state = new ReactiveDict({ + apps: [], // TODO: maybe use another ReactiveDict here + isLoading: true, + searchText: '', + sortedColumn: 'name', + isAscendingOrder: true, + + // TODO: to use these fields + page: 0, + itemsPerPage: 0, + wasEndReached: false, + }); + + (async () => { + try { + const appsFromMarketplace = await Apps.getAppsFromMarketplace(); + const installedApps = await Apps.getApps(); + + const apps = installedApps.map((app) => { + const appFromMarketplace = appsFromMarketplace.find(({ id }) => id === app.id); + + if (!appFromMarketplace) { + return { + ...app, + installed: true, + }; + } + + return { + ...app, + installed: true, + categories: appFromMarketplace.categories, + marketplaceVersion: appFromMarketplace.version, + }; + }); + + this.state.set('apps', apps); + } catch (error) { + handleAPIError(error); + } finally { + this.state.set('isLoading', false); + } + })(); + + this.startAppWorking = (appId) => { + const apps = this.state.get('apps'); + const app = apps.find(({ id }) => id === appId); + app.working = true; + this.state.set('apps', apps); + }; + + this.stopAppWorking = (appId) => { + const apps = this.state.get('apps'); + const app = apps.find(({ id }) => id === appId); + delete app.working; + this.state.set('apps', apps); + }; + + this.handleAppAddedOrUpdated = async (appId) => { + try { + const app = await Apps.getApp(appId); + const { categories, version: marketplaceVersion } = await Apps.getAppFromMarketplace(appId, app.version) || {}; + const apps = [ + ...this.state.get('apps').filter(({ id }) => id !== appId), + { + ...app, + installed: true, + categories, + marketplaceVersion, + }, + ]; + this.state.set('apps', apps); + } catch (error) { + handleAPIError(error); + } + }; + + this.handleAppRemoved = (appId) => { + this.state.set('apps', this.state.get('apps').filter(({ id }) => id !== appId)); + }; + + this.handleAppStatusChange = ({ appId, status }) => { + const apps = this.state.get('apps'); + const app = apps.find(({ id }) => id === appId); + if (!app) { + return; + } + + app.status = status; + this.state.set('apps', apps); + }; + + Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); + Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); + Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleAppRemoved); + Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); +}); + +Template.apps.onDestroyed(function() { + Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); + Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); + Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleAppRemoved); + Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); +}); + +Template.apps.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); + +Template.apps.helpers({ + isDevelopmentModeEnabled() { + return settings.get('Apps_Framework_Development_Mode') === true; + }, + isLoading() { + return Template.instance().state.get('isLoading'); + }, + handleTableScroll() { + const { state } = Template.instance(); + if (state.get('isLoading') || state.get('wasEndReached')) { + return; + } + + return ({ offsetHeight, scrollTop, scrollHeight }) => { + const shouldGoToNextPage = offsetHeight + scrollTop >= scrollHeight - 100; + if (shouldGoToNextPage) { + return state.set('page', state.get('page') + 1); + } + }; + }, + handleTableResize() { + const { state } = Template.instance(); + + return function() { + const $table = this.$('.table-scroll'); + state.set('itemsPerPage', Math.ceil(($table.height() / 40) + 5)); + }; + }, + handleTableSort() { + const { state } = Template.instance(); + + return (sortedColumn) => { + state.set({ + page: 0, + wasEndReached: false, + }); + + if (state.get('sortedColumn') === sortedColumn) { + state.set('isAscendingOrder', !state.get('isAscendingOrder')); + return; + } + + state.set({ + sortedColumn, + isAscendingOrder: true, + }); + }; + }, + isSortingBy(column) { + return Template.instance().state.get('sortedColumn') === column; + }, + sortIcon(column) { + const { state } = Template.instance(); + + return column === state.get('sortedColumn') && state.get('isAscendingOrder') ? 'sort-down' : 'sort-up'; + }, + apps() { + const { state } = Template.instance(); + const apps = state.get('apps'); + const searchText = state.get('searchText').toLocaleLowerCase(); + const sortedColumn = state.get('sortedColumn'); + const isAscendingOrder = state.get('isAscendingOrder'); + const sortingFactor = isAscendingOrder ? 1 : -1; + + return apps + .filter(({ name }) => name.toLocaleLowerCase().includes(searchText)) + .sort(({ [sortedColumn]: a }, { [sortedColumn]: b }) => sortingFactor * String(a).localeCompare(String(b))); + }, + appButtonProps, + appStatusSpanProps, +}); + +Template.apps.events({ + 'click .js-marketplace'() { + FlowRouter.go('marketplace'); + }, + 'click .js-upload'() { + FlowRouter.go('app-install'); + }, + 'submit .js-search-form'(event) { + event.stopPropagation(); + return false; + }, + 'input .js-search'(event, instance) { + instance.state.set('searchText', event.currentTarget.value); + }, + 'click .js-manage'(event, instance) { + event.stopPropagation(); + const { currentTarget } = event; + const { + id: appId, + version, + } = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); + FlowRouter.go('app-manage', { appId }, { version }); + }, + async 'click .js-install, click .js-update'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + if (!await checkCloudLogin()) { + return; + } + + const { currentTarget: button } = event; + const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); + + instance.startAppWorking(app.id); + + try { + const { status } = await Apps.installApp(app.id, app.marketplaceVersion); + warnStatusChange(app.name, status); + } catch (error) { + handleAPIError(error); + } finally { + instance.stopAppWorking(app.id); + } + }, + async 'click .js-purchase'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + if (!await checkCloudLogin()) { + return; + } + + const { currentTarget: button } = event; + const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); + + instance.startAppWorking(app.id); + + await promptSubscription(app, async () => { + try { + const { status } = await Apps.installApp(app.id, app.marketplaceVersion); + warnStatusChange(app.name, status); + } catch (error) { + handleAPIError(error); + } finally { + instance.stopAppWorking(app.id); + } + }, instance.stopAppWorking.bind(instance, app.id)); + }, + 'click .js-menu'(event, instance) { + event.stopPropagation(); + const { currentTarget } = event; + + const app = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); + triggerAppPopoverMenu(app, currentTarget, instance); + }, +}); diff --git a/app/apps/client/admin/helpers.js b/app/apps/client/admin/helpers.js new file mode 100644 index 000000000000..839974e465ed --- /dev/null +++ b/app/apps/client/admin/helpers.js @@ -0,0 +1,379 @@ +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import semver from 'semver'; +import toastr from 'toastr'; + +import { modal, popover, call } from '../../../ui-utils/client'; +import { t } from '../../../utils/client'; +import { Apps } from '../orchestrator'; + +const appEnabledStatuses = [ + AppStatus.AUTO_ENABLED, + AppStatus.MANUALLY_ENABLED, +]; + +const appErroredStatuses = [ + AppStatus.COMPILER_ERROR_DISABLED, + AppStatus.ERROR_DISABLED, + AppStatus.INVALID_SETTINGS_DISABLED, + AppStatus.INVALID_LICENSE_DISABLED, +]; + +export const handleAPIError = (error) => { + console.error(error); + const message = (error.xhr && error.xhr.responseJSON && error.xhr.responseJSON.error) || error.message; + toastr.error(message); +}; + +export const warnStatusChange = (appName, status) => { + if (appErroredStatuses.includes(status)) { + toastr.error(t(`App_status_${ status }`), appName); + return; + } + + toastr.info(t(`App_status_${ status }`), appName); +}; + +const promptCloudLogin = () => { + modal.open({ + title: t('Apps_Marketplace_Login_Required_Title'), + text: t('Apps_Marketplace_Login_Required_Description'), + type: 'info', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Login'), + cancelButtonText: t('Cancel'), + closeOnConfirm: true, + html: false, + }, (confirmed) => { + if (confirmed) { + FlowRouter.go('cloud-config'); + } + }); +}; + +export const checkCloudLogin = async () => { + try { + const isLoggedIn = await call('cloud:checkUserLoggedIn'); + + if (!isLoggedIn) { + promptCloudLogin(); + } + + return isLoggedIn; + } catch (error) { + handleAPIError(error); + return false; + } +}; + +export const promptSubscription = async (app, callback, cancelCallback) => { + let data = null; + try { + data = await Apps.buildExternalUrl(app.id, app.purchaseType, false); + } catch (error) { + handleAPIError(error); + cancelCallback(); + return; + } + + modal.open({ + allowOutsideClick: false, + data, + template: 'iframeModal', + }, callback, cancelCallback); +}; + +const promptModifySubscription = async ({ id, purchaseType }) => { + if (!await checkCloudLogin()) { + return; + } + + let data = null; + try { + data = await Apps.buildExternalUrl(id, purchaseType, true); + } catch (error) { + handleAPIError(error); + return; + } + + await new Promise((resolve) => { + modal.open({ + allowOutsideClick: false, + data, + template: 'iframeModal', + }, resolve); + }); +}; + +const promptAppDeactivation = () => new Promise((resolve) => { + modal.open({ + text: t('Apps_Marketplace_Deactivate_App_Prompt'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes'), + cancelButtonText: t('No'), + closeOnConfirm: true, + html: false, + }, resolve, () => resolve(false)); +}); + +const promptAppUninstall = () => new Promise((resolve) => { + modal.open({ + text: t('Apps_Marketplace_Uninstall_App_Prompt'), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes'), + cancelButtonText: t('No'), + closeOnConfirm: true, + html: false, + }, resolve, () => resolve(false)); +}); + +const promptSubscribedAppUninstall = () => new Promise((resolve) => { + modal.open({ + text: t('Apps_Marketplace_Uninstall_Subscribed_App_Prompt'), + type: 'info', + showCancelButton: true, + confirmButtonText: t('Apps_Marketplace_Modify_App_Subscription'), + cancelButtonText: t('Apps_Marketplace_Uninstall_Subscribed_App_Anyway'), + cancelButtonColor: '#DD6B55', + closeOnConfirm: true, + html: false, + }, resolve, () => resolve(false)); +}); + +export const triggerAppPopoverMenu = (app, currentTarget, instance) => { + if (!app) { + return; + } + + const canAppBeSubscribed = app.purchaseType === 'subscription'; + const isSubscribed = app.subscriptionInfo && ['active', 'trialing'].includes(app.subscriptionInfo.status); + const isAppEnabled = appEnabledStatuses.includes(app.status); + + const handleSubscription = async () => { + await promptModifySubscription(app); + try { + await Apps.syncApp(app.id); + } catch (error) { + handleAPIError(error); + } + }; + + const handleViewLogs = () => { + FlowRouter.go('app-logs', { appId: app.id }, { version: app.version }); + }; + + const handleDisable = async () => { + if (!await promptAppDeactivation()) { + return; + } + + try { + const effectiveStatus = await Apps.disableApp(app.id); + warnStatusChange(app.name, effectiveStatus); + } catch (error) { + handleAPIError(error); + } + }; + + const handleEnable = async () => { + try { + const effectiveStatus = await Apps.enableApp(app.id); + warnStatusChange(app.name, effectiveStatus); + } catch (error) { + handleAPIError(error); + } + }; + + const handleUninstall = async () => { + if (isSubscribed) { + const modifySubscription = await promptSubscribedAppUninstall(); + if (modifySubscription) { + await promptModifySubscription(app); + try { + await Apps.syncApp(app.id); + } catch (error) { + handleAPIError(error); + } + return; + } + + try { + await Apps.uninstallApp(app.id); + } catch (error) { + handleAPIError(error); + } + return; + } + + if (!await promptAppUninstall()) { + return; + } + try { + await Apps.uninstallApp(app.id); + } catch (error) { + handleAPIError(error); + } + }; + + popover.open({ + currentTarget, + instance, + columns: [{ + groups: [ + { + items: [ + ...canAppBeSubscribed ? [{ + icon: 'card', + name: t('Subscription'), + action: handleSubscription, + }] : [], + { + icon: 'list-alt', + name: t('View_Logs'), + action: handleViewLogs, + }, + ], + }, + { + items: [ + isAppEnabled + ? { + icon: 'ban', + name: t('Disable'), + modifier: 'alert', + action: handleDisable, + } + : { + icon: 'check', + name: t('Enable'), + action: handleEnable, + }, + { + icon: 'trash', + name: t('Uninstall'), + modifier: 'alert', + action: handleUninstall, + }, + ], + }, + ], + }], + }); +}; + +export const appButtonProps = ({ + installed, + version, + marketplaceVersion, + isPurchased, + price, + purchaseType, + subscriptionInfo, +}) => { + const canUpdate = installed + && version && marketplaceVersion + && semver.lt(version, marketplaceVersion) + && isPurchased; + if (canUpdate) { + return { + action: 'update', + icon: 'reload', + label: 'Update', + }; + } + + if (installed) { + return; + } + + const canDownload = isPurchased; + if (canDownload) { + return { + action: 'install', + label: 'Install', + }; + } + + const canTrial = purchaseType === 'subscription' && !subscriptionInfo.status; + if (canTrial) { + return { + action: 'purchase', + label: 'Trial', + }; + } + + const canBuy = price > 0; + if (canBuy) { + return { + action: 'purchase', + label: 'Buy', + }; + } + + return { + action: 'purchase', + label: 'Install', + }; +}; + +export const appStatusSpanProps = ({ + installed, + status, + subscriptionInfo, +}) => { + if (!installed) { + return; + } + + const isFailed = appErroredStatuses.includes(status); + if (isFailed) { + return { + type: 'failed', + icon: 'warning', + label: 'Failed', + }; + } + + const isEnabled = appEnabledStatuses.includes(status); + if (!isEnabled) { + return { + type: 'warning', + icon: 'warning', + label: 'Disabled', + }; + } + + const isOnTrialPeriod = subscriptionInfo && subscriptionInfo.status === 'trialing'; + if (isOnTrialPeriod) { + return { + icon: 'checkmark-circled', + label: 'Trial period', + }; + } + + return { + icon: 'checkmark-circled', + label: 'Enabled', + }; +}; + +export const formatPrice = (price) => `\$${ Number.parseFloat(price).toFixed(2) }`; + +export const formatPricingPlan = ({ strategy, price, tiers }) => { + const { perUnit = false } = (Array.isArray(tiers) && tiers.find((tier) => tier.price === price)) || {}; + + const pricingPlanTranslationString = [ + 'Apps_Marketplace_pricingPlan', + strategy, + perUnit && 'perUser', + ].filter(Boolean).join('_'); + + return t(pricingPlanTranslationString, { + price: formatPrice(price), + }); +}; diff --git a/app/apps/client/admin/marketplace.html b/app/apps/client/admin/marketplace.html new file mode 100644 index 000000000000..5ee63be68ae5 --- /dev/null +++ b/app/apps/client/admin/marketplace.html @@ -0,0 +1,145 @@ + diff --git a/app/apps/client/admin/marketplace.js b/app/apps/client/admin/marketplace.js new file mode 100644 index 000000000000..0907080453d2 --- /dev/null +++ b/app/apps/client/admin/marketplace.js @@ -0,0 +1,333 @@ +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { ReactiveDict } from 'meteor/reactive-dict'; +import { Template } from 'meteor/templating'; +import { Tracker } from 'meteor/tracker'; + +import { SideNav, call } from '../../../ui-utils/client'; +import { t } from '../../../utils'; +import { AppEvents } from '../communication'; +import { Apps } from '../orchestrator'; +import { + appButtonProps, + appStatusSpanProps, + checkCloudLogin, + formatPrice, + formatPricingPlan, + handleAPIError, + promptSubscription, + triggerAppPopoverMenu, + warnStatusChange, +} from './helpers'; + +import './marketplace.html'; + + +Template.marketplace.onCreated(function() { + this.state = new ReactiveDict({ + isLoggedInCloud: true, + apps: [], // TODO: maybe use another ReactiveDict here + isLoading: true, + searchText: '', + sortedColumn: 'name', + isAscendingOrder: true, + + // TODO: to use these fields + page: 0, + itemsPerPage: 0, + wasEndReached: false, + }); + + (async () => { + try { + this.state.set('isLoggedInCloud', await call('cloud:checkUserLoggedIn')); + } catch (error) { + handleAPIError(error); + } + + try { + const appsFromMarketplace = await Apps.getAppsFromMarketplace(); + const installedApps = await Apps.getApps(); + + const apps = appsFromMarketplace.map((app) => { + const installedApp = installedApps.find(({ id }) => id === app.id); + + if (!installedApp) { + return { + ...app, + status: undefined, + marketplaceVersion: app.version, + }; + } + + return { + ...app, + installed: true, + status: installedApp.status, + version: installedApp.version, + marketplaceVersion: app.version, + }; + }); + + this.state.set('apps', apps); + } catch (error) { + handleAPIError(error); + } finally { + this.state.set('isLoading', false); + } + })(); + + this.startAppWorking = (appId) => { + const apps = this.state.get('apps'); + const app = apps.find(({ id }) => id === appId); + app.working = true; + this.state.set('apps', apps); + }; + + this.stopAppWorking = (appId) => { + const apps = this.state.get('apps'); + const app = apps.find(({ id }) => id === appId); + delete app.working; + this.state.set('apps', apps); + }; + + this.handleAppAddedOrUpdated = async (appId) => { + try { + const { status, version } = await Apps.getApp(appId); + const app = await Apps.getAppFromMarketplace(appId, version); + const apps = [ + ...this.state.get('apps').filter(({ id }) => id !== appId), + { + ...app, + installed: true, + status, + version, + marketplaceVersion: app.version, + }, + ]; + this.state.set('apps', apps); + } catch (error) { + handleAPIError(error); + } + }; + + this.handleAppRemoved = (appId) => { + const apps = this.state.get('apps').map((app) => { + if (app.id === appId) { + delete app.installed; + delete app.status; + app.version = app.marketplaceVersion; + } + + return app; + }); + this.state.set('apps', apps); + }; + + this.handleAppStatusChange = ({ appId, status }) => { + const apps = this.state.get('apps'); + const app = apps.find(({ id }) => id === appId); + if (!app) { + return; + } + + app.status = status; + this.state.set('apps', apps); + }; + + Apps.getWsListener().registerListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); + Apps.getWsListener().registerListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); + Apps.getWsListener().registerListener(AppEvents.APP_REMOVED, this.handleAppRemoved); + Apps.getWsListener().registerListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); +}); + +Template.marketplace.onDestroyed(function() { + Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, this.handleAppAddedOrUpdated); + Apps.getWsListener().unregisterListener(AppEvents.APP_UPDATED, this.handleAppAddedOrUpdated); + Apps.getWsListener().unregisterListener(AppEvents.APP_REMOVED, this.handleAppRemoved); + Apps.getWsListener().unregisterListener(AppEvents.APP_STATUS_CHANGE, this.handleAppStatusChange); +}); + +Template.marketplace.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); + +Template.marketplace.helpers({ + isLoggedInCloud() { + return Template.instance().state.get('isLoggedInCloud'); + }, + isLoading() { + return Template.instance().state.get('isLoading'); + }, + handleTableScroll() { + const { state } = Template.instance(); + if (state.get('isLoading') || state.get('wasEndReached')) { + return; + } + + return ({ offsetHeight, scrollTop, scrollHeight }) => { + const shouldGoToNextPage = offsetHeight + scrollTop >= scrollHeight - 100; + if (shouldGoToNextPage) { + return state.set('page', state.get('page') + 1); + } + }; + }, + handleTableResize() { + const { state } = Template.instance(); + + return function() { + const $table = this.$('.table-scroll'); + state.set('itemsPerPage', Math.ceil(($table.height() / 40) + 5)); + }; + }, + handleTableSort() { + const { state } = Template.instance(); + + return (sortedColumn) => { + state.set({ + page: 0, + wasEndReached: false, + }); + + if (state.get('sortedColumn') === sortedColumn) { + state.set('isAscendingOrder', !state.get('isAscendingOrder')); + return; + } + + state.set({ + sortedColumn, + isAscendingOrder: true, + }); + }; + }, + isSortingBy(column) { + return Template.instance().state.get('sortedColumn') === column; + }, + sortIcon(column) { + const { state } = Template.instance(); + + return column === state.get('sortedColumn') && state.get('isAscendingOrder') ? 'sort-down' : 'sort-up'; + }, + apps() { + const { state } = Template.instance(); + const apps = state.get('apps'); + const searchText = state.get('searchText').toLocaleLowerCase(); + const sortedColumn = state.get('sortedColumn'); + const isAscendingOrder = state.get('isAscendingOrder'); + const sortingFactor = isAscendingOrder ? 1 : -1; + + return apps + .filter(({ name }) => name.toLocaleLowerCase().includes(searchText)) + .sort(({ [sortedColumn]: a }, { [sortedColumn]: b }) => sortingFactor * String(a).localeCompare(String(b))); + }, + purchaseTypeDisplay({ purchaseType, price }) { + if (purchaseType === 'subscription') { + return t('Subscription'); + } + + if (price > 0) { + return t('Paid'); + } + + return t('Free'); + }, + priceDisplay({ purchaseType, pricingPlans, price }) { + if (purchaseType === 'subscription') { + if (!pricingPlans || !Array.isArray(pricingPlans) || pricingPlans.length === 0) { + return '-'; + } + + return formatPricingPlan(pricingPlans[0]); + } + + if (price > 0) { + return formatPrice(price); + } + + return '-'; + }, + appButtonProps, + appStatusSpanProps, +}); + +Template.marketplace.events({ + 'click .js-cloud-login'() { + FlowRouter.go('cloud-config'); + }, + 'submit .js-search-form'(event) { + event.stopPropagation(); + return false; + }, + 'keyup .js-search'(event, instance) { + instance.state.set('searchText', event.currentTarget.value); + }, + 'click .js-open'(event, instance) { + event.stopPropagation(); + const { currentTarget } = event; + const { + id: appId, + version, + marketplaceVersion, + } = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); + FlowRouter.go('marketplace-app', { appId }, { version: version || marketplaceVersion }); + }, + async 'click .js-install, click .js-update'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + const isLoggedInCloud = await checkCloudLogin(); + instance.state.set('isLoggedInCloud', isLoggedInCloud); + if (!isLoggedInCloud) { + return; + } + + const { currentTarget: button } = event; + const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); + + instance.startAppWorking(app.id); + + try { + const { status } = await Apps.installApp(app.id, app.marketplaceVersion); + warnStatusChange(app.name, status); + } catch (error) { + handleAPIError(error); + } finally { + instance.stopAppWorking(app.id); + } + }, + async 'click .js-purchase'(event, instance) { + event.preventDefault(); + event.stopPropagation(); + + const isLoggedInCloud = await checkCloudLogin(); + instance.state.set('isLoggedInCloud', isLoggedInCloud); + if (!isLoggedInCloud) { + return; + } + + const { currentTarget: button } = event; + const app = instance.state.get('apps').find(({ id }) => id === button.dataset.id); + + instance.startAppWorking(app.id); + + await promptSubscription(app, async () => { + try { + const { status } = await Apps.installApp(app.id, app.marketplaceVersion); + warnStatusChange(app.name, status); + } catch (error) { + handleAPIError(error); + } finally { + instance.stopAppWorking(app.id); + } + }, instance.stopAppWorking.bind(instance, app.id)); + }, + 'click .js-menu'(event, instance) { + event.stopPropagation(); + const { currentTarget } = event; + + const app = instance.state.get('apps').find(({ id }) => id === currentTarget.dataset.id); + triggerAppPopoverMenu(app, currentTarget, instance); + }, +}); diff --git a/app/apps/client/admin/modalTemplates/iframeModal.html b/app/apps/client/admin/modalTemplates/iframeModal.html new file mode 100644 index 000000000000..6902d2cd0db6 --- /dev/null +++ b/app/apps/client/admin/modalTemplates/iframeModal.html @@ -0,0 +1,7 @@ + diff --git a/app/apps/client/admin/modalTemplates/iframeModal.js b/app/apps/client/admin/modalTemplates/iframeModal.js new file mode 100644 index 000000000000..bc1029a56f7d --- /dev/null +++ b/app/apps/client/admin/modalTemplates/iframeModal.js @@ -0,0 +1,49 @@ +import { Template } from 'meteor/templating'; + +import { modal } from '../../../../ui-utils'; + +Template.iframeModal.onCreated(function() { + const instance = this; + + instance.iframeMsgListener = function _iframeMsgListener(e) { + let data; + try { + data = JSON.parse(e.data); + } catch (e) { + return; + } + + if (data.result) { + if (typeof instance.data.successCallback === 'function') { + instance.data.successCallback().then(() => modal.confirm(data)); + } else { + modal.confirm(data); + } + } else { + modal.cancel(); + } + }; + + window.addEventListener('message', instance.iframeMsgListener); +}); + +Template.iframeModal.onRendered(function() { + const iframe = this.firstNode.querySelector('iframe'); + const loading = this.firstNode.querySelector('.loading'); + iframe.addEventListener('load', () => { + iframe.style.display = 'block'; + loading.style.display = 'none'; + }); +}); + +Template.iframeModal.onDestroyed(function() { + const instance = this; + + window.removeEventListener('message', instance.iframeMsgListener); +}); + +Template.iframeModal.helpers({ + data() { + return Template.instance().data; + }, +}); diff --git a/app/apps/client/communication/index.js b/app/apps/client/communication/index.js new file mode 100644 index 000000000000..321bbb7f15b7 --- /dev/null +++ b/app/apps/client/communication/index.js @@ -0,0 +1 @@ +export { AppWebsocketReceiver, AppEvents } from './websockets'; diff --git a/app/apps/client/communication/websockets.js b/app/apps/client/communication/websockets.js new file mode 100644 index 000000000000..aa1a23d451cb --- /dev/null +++ b/app/apps/client/communication/websockets.js @@ -0,0 +1,58 @@ +import { Meteor } from 'meteor/meteor'; +import EventEmitter from 'wolfy87-eventemitter'; + +import { slashCommands, APIClient } from '../../../utils'; +import { CachedCollectionManager } from '../../../ui-cached-collection'; + +export const AppEvents = Object.freeze({ + APP_ADDED: 'app/added', + APP_REMOVED: 'app/removed', + APP_UPDATED: 'app/updated', + APP_STATUS_CHANGE: 'app/statusUpdate', + APP_SETTING_UPDATED: 'app/settingUpdated', + COMMAND_ADDED: 'command/added', + COMMAND_DISABLED: 'command/disabled', + COMMAND_UPDATED: 'command/updated', + COMMAND_REMOVED: 'command/removed', +}); + +export class AppWebsocketReceiver extends EventEmitter { + constructor() { + super(); + + this.streamer = new Meteor.Streamer('apps'); + + CachedCollectionManager.onLogin(() => { + this.listenStreamerEvents(); + }); + } + + listenStreamerEvents() { + Object.values(AppEvents).forEach((eventName) => { + this.streamer.on(eventName, this.emit.bind(this, eventName)); + }); + + this.streamer.on(AppEvents.COMMAND_ADDED, this.onCommandAddedOrUpdated); + this.streamer.on(AppEvents.COMMAND_UPDATED, this.onCommandAddedOrUpdated); + this.streamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemovedOrDisabled); + this.streamer.on(AppEvents.COMMAND_DISABLED, this.onCommandRemovedOrDisabled); + } + + registerListener(event, listener) { + this.on(event, listener); + } + + unregisterListener(event, listener) { + this.off(event, listener); + } + + onCommandAddedOrUpdated = (command) => { + APIClient.v1.get('commands.get', { command }).then((result) => { + slashCommands.commands[command] = result.command; + }); + } + + onCommandRemovedOrDisabled = (command) => { + delete slashCommands.commands[command]; + } +} diff --git a/app/apps/client/i18n.js b/app/apps/client/i18n.js new file mode 100644 index 000000000000..9b9b1a9755e2 --- /dev/null +++ b/app/apps/client/i18n.js @@ -0,0 +1,38 @@ +import { TAPi18next } from 'meteor/rocketchat:tap-i18n'; + +import { Apps } from './orchestrator'; +import { Utilities } from '../lib/misc/Utilities'; +import { AppEvents } from './communication'; + + +export const loadAppI18nResources = (appId, languages) => { + Object.entries(languages).forEach(([language, translations]) => { + try { + // Translations keys must be scoped under app id + const scopedTranslations = Object.entries(translations) + .reduce((translations, [key, value]) => { + translations[Utilities.getI18nKeyForApp(key, appId)] = value; + return translations; + }, {}); + + TAPi18next.addResourceBundle(language, 'project', scopedTranslations); + } catch (error) { + Apps.handleError(error); + } + }); +}; + +const handleAppAdded = async (appId) => { + const languages = await Apps.getAppLanguages(appId); + loadAppI18nResources(appId, languages); +}; + +export const handleI18nResources = async () => { + const apps = await Apps.getAppsLanguages(); + apps.forEach(({ id, languages }) => { + loadAppI18nResources(id, languages); + }); + + Apps.getWsListener().unregisterListener(AppEvents.APP_ADDED, handleAppAdded); + Apps.getWsListener().registerListener(AppEvents.APP_ADDED, handleAppAdded); +}; diff --git a/app/apps/client/index.js b/app/apps/client/index.js new file mode 100644 index 000000000000..887964154a90 --- /dev/null +++ b/app/apps/client/index.js @@ -0,0 +1,14 @@ +import './admin/modalTemplates/iframeModal.html'; +import './admin/modalTemplates/iframeModal'; +import './admin/marketplace'; +import './admin/apps'; +import './admin/appInstall.html'; +import './admin/appInstall'; +import './admin/appLogs.html'; +import './admin/appLogs'; +import './admin/appManage'; +import './admin/appWhatIsIt.html'; +import './admin/appWhatIsIt'; +import './routes'; + +export { Apps } from './orchestrator'; diff --git a/app/apps/client/orchestrator.js b/app/apps/client/orchestrator.js new file mode 100644 index 000000000000..28b06e558de4 --- /dev/null +++ b/app/apps/client/orchestrator.js @@ -0,0 +1,188 @@ +import { Meteor } from 'meteor/meteor'; +import toastr from 'toastr'; + +import { AppWebsocketReceiver } from './communication'; +import { APIClient } from '../../utils'; +import { AdminBox } from '../../ui-utils'; +import { CachedCollectionManager } from '../../ui-cached-collection'; +import { hasAtLeastOnePermission } from '../../authorization'; +import { handleI18nResources } from './i18n'; + +const createDeferredValue = () => { + let resolve; + let reject; + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve; + reject = _reject; + }); + + return [promise, resolve, reject]; +}; + +class AppClientOrchestrator { + constructor() { + this.isLoaded = false; + [this.deferredIsEnabled, this.setEnabled] = createDeferredValue(); + } + + load = async (isEnabled) => { + if (!this.isLoaded) { + this.ws = new AppWebsocketReceiver(); + this.registerAdminMenuItems(); + this.isLoaded = true; + } + + this.setEnabled(isEnabled); + + // Since the deferred value (a promise) is immutable after resolved, + // it need to be recreated to resolve a new value + [this.deferredIsEnabled, this.setEnabled] = createDeferredValue(); + + await handleI18nResources(); + this.setEnabled(isEnabled); + } + + getWsListener = () => this.ws + + registerAdminMenuItems = () => { + AdminBox.addOption({ + icon: 'cube', + href: 'apps', + i18nLabel: 'Apps', + permissionGranted: () => hasAtLeastOnePermission(['manage-apps']), + }); + + AdminBox.addOption({ + icon: 'cube', + href: 'marketplace', + i18nLabel: 'Marketplace', + permissionGranted: () => hasAtLeastOnePermission(['manage-apps']), + }); + } + + handleError = (error) => { + console.error(error); + if (hasAtLeastOnePermission(['manage-apps'])) { + toastr.error(error.message); + } + } + + isEnabled = () => this.deferredIsEnabled + + getApps = async () => { + const { apps } = await APIClient.get('apps'); + return apps; + } + + getAppsFromMarketplace = async () => { + const appsOverviews = await APIClient.get('apps', { marketplace: 'true' }); + return appsOverviews.map(({ latest, price, pricingPlans, purchaseType }) => ({ + ...latest, + price, + pricingPlans, + purchaseType, + })); + } + + getAppsOnBundle = async (bundleId) => { + const { apps } = await APIClient.get(`apps/bundles/${ bundleId }/apps`); + return apps; + } + + getAppsLanguages = async () => { + const { apps } = await APIClient.get('apps/languages'); + return apps; + } + + getApp = async (appId) => { + const { app } = await APIClient.get(`apps/${ appId }`); + return app; + } + + getAppFromMarketplace = async (appId, version) => { + const { app } = await APIClient.get(`apps/${ appId }`, { + marketplace: 'true', + version, + }); + return app; + } + + getLatestAppFromMarketplace = async (appId, version) => { + const { app } = await APIClient.get(`apps/${ appId }`, { + marketplace: 'true', + update: 'true', + appVersion: version, + }); + return app; + } + + getAppSettings = async (appId) => { + const { settings } = await APIClient.get(`apps/${ appId }/settings`); + return settings; + } + + setAppSettings = async (appId, settings) => { + const { updated } = await APIClient.post(`apps/${ appId }/settings`, undefined, { settings }); + return updated; + } + + getAppApis = async (appId) => { + const { apis } = await APIClient.get(`apps/${ appId }/apis`); + return apis; + } + + getAppLanguages = async (appId) => { + const { languages } = await APIClient.get(`apps/${ appId }/languages`); + return languages; + } + + installApp = async (appId, version) => { + const { app } = await APIClient.post('apps/', { + appId, + marketplace: true, + version, + }); + return app; + } + + uninstallApp = (appId) => APIClient.delete(`apps/${ appId }`) + + syncApp = (appId) => APIClient.post(`apps/${ appId }/sync`) + + setAppStatus = async (appId, status) => { + const { status: effectiveStatus } = await APIClient.post(`apps/${ appId }/status`, { status }); + return effectiveStatus; + } + + enableApp = (appId) => this.setAppStatus(appId, 'manually_enabled') + + disableApp = (appId) => this.setAppStatus(appId, 'manually_disabled') + + buildExternalUrl = (appId, purchaseType = 'buy', details = false) => + APIClient.get('apps', { + buildExternalUrl: 'true', + appId, + purchaseType, + details, + }) + + getCategories = async () => { + const categories = await APIClient.get('apps', { categories: 'true' }); + return categories; + } +} + +export const Apps = new AppClientOrchestrator(); + +Meteor.startup(() => { + CachedCollectionManager.onLogin(() => { + Meteor.call('apps/is-enabled', (error, isEnabled) => { + if (error) { + Apps.handleError(error); + return; + } + + Apps.load(isEnabled); + }); + }); +}); diff --git a/app/apps/client/routes.js b/app/apps/client/routes.js new file mode 100644 index 000000000000..1a44f4d74bff --- /dev/null +++ b/app/apps/client/routes.js @@ -0,0 +1,55 @@ +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { BlazeLayout } from 'meteor/kadira:blaze-layout'; + +import { Apps } from './orchestrator'; + +FlowRouter.route('/admin/apps/what-is-it', { + name: 'apps-what-is-it', + action: async () => { + // TODO: render loading indicator + if (await Apps.isEnabled()) { + FlowRouter.go('apps'); + } else { + BlazeLayout.render('main', { center: 'appWhatIsIt' }); + } + }, +}); + +const createAppsRouteAction = (centerTemplate) => async () => { + // TODO: render loading indicator + if (await Apps.isEnabled()) { + BlazeLayout.render('main', { center: centerTemplate, old: true }); // TODO remove old + } else { + FlowRouter.go('apps-what-is-it'); + } +}; + +FlowRouter.route('/admin/apps', { + name: 'apps', + action: createAppsRouteAction('apps'), +}); + +FlowRouter.route('/admin/apps/install', { + name: 'app-install', + action: createAppsRouteAction('appInstall'), +}); + +FlowRouter.route('/admin/apps/:appId', { + name: 'app-manage', + action: createAppsRouteAction('appManage'), +}); + +FlowRouter.route('/admin/apps/:appId/logs', { + name: 'app-logs', + action: createAppsRouteAction('appLogs'), +}); + +FlowRouter.route('/admin/marketplace', { + name: 'marketplace', + action: createAppsRouteAction('marketplace'), +}); + +FlowRouter.route('/admin/marketplace/:appId', { + name: 'marketplace-app', + action: createAppsRouteAction('appManage'), +}); diff --git a/app/apps/lib/misc/Utilities.js b/app/apps/lib/misc/Utilities.js new file mode 100644 index 000000000000..02d49e8b027c --- /dev/null +++ b/app/apps/lib/misc/Utilities.js @@ -0,0 +1,72 @@ +export class Utilities { + static getI18nKeyForApp(key, appId) { + return key && `apps-${ appId }-${ key }`; + } + + static curl({ method, params, auth, headers = {}, url, query, content }, opts = {}) { + const newLine = '\\\n '; + + const cmd = ['curl']; + + // curl options + if (opts.verbose) { + cmd.push('-v'); + } + if (opts.headers) { + cmd.push('-i'); + } + + // method + cmd.push('-X'); + cmd.push((method || 'GET').toUpperCase()); + + // URL + let u = url; + + if (typeof params === 'object') { + Object.entries(params).forEach(([key, value]) => { + u = u.replace(`:${ key }`, value); + }); + } + + if (typeof query === 'object') { + const queryString = Object.entries(query).map(([key, value]) => `${ key }=${ value }`).join('&'); + u += `?${ queryString }`; + } + cmd.push(u); + + // username + if (auth) { + cmd.push(newLine); + cmd.push('-u'); + cmd.push(auth); + } + + // headers + const headerKeys = []; + Object.entries(headers).forEach(([key, val]) => { + key = key.toLowerCase(); + headerKeys.push(key); + cmd.push(newLine); + cmd.push('-H'); + cmd.push(`"${ key }${ val ? ': ' : ';' }${ val || '' }"`); + }); + + if (content) { + if (typeof content === 'object') { + if (!headerKeys.includes('content-type')) { + cmd.push(newLine); + cmd.push('-H'); + cmd.push('"content-type: application/json"'); + } + content = JSON.stringify(content); + } + + cmd.push(newLine); + cmd.push('--data-binary'); + cmd.push(`'${ content }'`); + } + + return cmd.join(' '); + } +} diff --git a/app/apps/lib/misc/transformMappedData.js b/app/apps/lib/misc/transformMappedData.js new file mode 100644 index 000000000000..0bba2c646081 --- /dev/null +++ b/app/apps/lib/misc/transformMappedData.js @@ -0,0 +1,85 @@ +import cloneDeep from 'lodash.clonedeep'; + +/** + * Transforms a `data` source object to another object, + * essentially applying a to -> from mapping provided by + * `map`. It does not mutate the `data` object. + * + * It also inserts in the `transformedObject` a new property + * called `_unmappedProperties_` which contains properties from + * the original `data` that have not been mapped to its transformed + * counterpart. E.g.: + * + * ```javascript + * const data = { _id: 'abcde123456', size: 10 }; + * const map = { id: '_id' } + * + * transformMappedData(data, map); + * // { id: 'abcde123456', _unmappedProperties_: { size: 10 } } + * ``` + * + * In order to compute the unmapped properties, this function will + * ignore any property on `data` that has been named on the "from" part + * of the `map`, and will consider properties not mentioned as unmapped. + * + * You can also define the "from" part as a function, so you can derive a + * new value for your property from the original `data`. This function will + * receive a copy of the original `data` for it to calculate the value + * for its "to" field. Please note that in this case `transformMappedData` + * will not be able to determine the source field from your map, so it won't + * ignore any field you've used to derive your new value. For that, you're + * going to need to delete the value from the received parameter. E.g: + * + * ```javascript + * const data = { _id: 'abcde123456', size: 10 }; + * + * // It will look like the `size` property is not mapped + * const map = { + * id: '_id', + * newSize: (data) => data.size + 10 + * }; + * + * transformMappedData(data, map); + * // { id: 'abcde123456', newSize: 20, _unmappedProperties_: { size: 10 } } + * + * // You need to explicitly remove it from the original `data` + * const map = { + * id: '_id', + * newSize: (data) => { + * const result = data.size + 10; + * delete data.size; + * return result; + * } + * }; + * + * transformMappedData(data, map); + * // { id: 'abcde123456', newSize: 20, _unmappedProperties_: {} } + * ``` + * + * @param Object data The data to be transformed + * @param Object map The map with transformations to be applied + * + * @returns Object The data after transformations have been applied + */ + +export const transformMappedData = (data, map) => { + const originalData = cloneDeep(data); + const transformedData = {}; + + Object.entries(map).forEach(([to, from]) => { + if (typeof from === 'function') { + const result = from(originalData); + + if (typeof result !== 'undefined') { + transformedData[to] = result; + } + } else if (typeof from === 'string' && typeof originalData[from] !== 'undefined') { + transformedData[to] = originalData[from]; + delete originalData[from]; + } + }); + + transformedData._unmappedProperties_ = originalData; + + return transformedData; +}; diff --git a/packages/rocketchat-apps/server/bridges/activation.js b/app/apps/server/bridges/activation.js similarity index 100% rename from packages/rocketchat-apps/server/bridges/activation.js rename to app/apps/server/bridges/activation.js diff --git a/app/apps/server/bridges/api.js b/app/apps/server/bridges/api.js new file mode 100644 index 000000000000..47801c7d644a --- /dev/null +++ b/app/apps/server/bridges/api.js @@ -0,0 +1,114 @@ +import { Meteor } from 'meteor/meteor'; +import express from 'express'; +import { WebApp } from 'meteor/webapp'; + +const apiServer = express(); + +apiServer.disable('x-powered-by'); + +WebApp.connectHandlers.use(apiServer); + +export class AppApisBridge { + constructor(orch) { + this.orch = orch; + this.appRouters = new Map(); + + // apiServer.use('/api/apps', (req, res, next) => { + // this.orch.debugLog({ + // method: req.method.toLowerCase(), + // url: req.url, + // query: req.query, + // body: req.body, + // }); + // next(); + // }); + + apiServer.use('/api/apps/private/:appId/:hash', (req, res) => { + const notFound = () => res.send(404); + + const router = this.appRouters.get(req.params.appId); + + if (router) { + req._privateHash = req.params.hash; + return router(req, res, notFound); + } + + notFound(); + }); + + apiServer.use('/api/apps/public/:appId', (req, res) => { + const notFound = () => res.send(404); + + const router = this.appRouters.get(req.params.appId); + + if (router) { + return router(req, res, notFound); + } + + notFound(); + }); + } + + registerApi({ api, computedPath, endpoint }, appId) { + this.orch.debugLog(`The App ${ appId } is registering the api: "${ endpoint.path }" (${ computedPath })`); + + this._verifyApi(api, endpoint); + + if (!this.appRouters.get(appId)) { + this.appRouters.set(appId, express.Router()); // eslint-disable-line + } + + const router = this.appRouters.get(appId); + + const method = api.method || 'all'; + + let routePath = endpoint.path.trim(); + if (!routePath.startsWith('/')) { + routePath = `/${ routePath }`; + } + + router[method](routePath, Meteor.bindEnvironment(this._appApiExecutor(api, endpoint, appId))); + } + + unregisterApis(appId) { + this.orch.debugLog(`The App ${ appId } is unregistering all apis`); + + if (this.appRouters.get(appId)) { + this.appRouters.delete(appId); + } + } + + _verifyApi(api, endpoint) { + if (typeof api !== 'object') { + throw new Error('Invalid Api parameter provided, it must be a valid IApi object.'); + } + + if (typeof endpoint.path !== 'string') { + throw new Error('Invalid Api parameter provided, it must be a valid IApi object.'); + } + } + + _appApiExecutor(api, endpoint, appId) { + return (req, res) => { + const request = { + method: req.method.toLowerCase(), + headers: req.headers, + query: req.query || {}, + params: req.params || {}, + content: req.body, + privateHash: req._privateHash, + }; + + this.orch.getManager().getApiManager().executeApi(appId, endpoint.path, request) + .then(({ status, headers = {}, content }) => { + res.set(headers); + res.status(status); + res.send(content); + }) + .catch((reason) => { + // Should we handle this as an error? + res.status(500).send(reason.message); + }); + }; + } +} diff --git a/packages/rocketchat-apps/server/bridges/bridges.js b/app/apps/server/bridges/bridges.js similarity index 84% rename from packages/rocketchat-apps/server/bridges/bridges.js rename to app/apps/server/bridges/bridges.js index 83ccc7cd4b1a..5781842fb766 100644 --- a/packages/rocketchat-apps/server/bridges/bridges.js +++ b/app/apps/server/bridges/bridges.js @@ -3,12 +3,14 @@ import { AppBridges } from '@rocket.chat/apps-engine/server/bridges'; import { AppActivationBridge } from './activation'; import { AppDetailChangesBridge } from './details'; import { AppCommandsBridge } from './commands'; +import { AppApisBridge } from './api'; import { AppEnvironmentalVariableBridge } from './environmental'; import { AppHttpBridge } from './http'; import { AppListenerBridge } from './listeners'; import { AppMessageBridge } from './messages'; import { AppPersistenceBridge } from './persistence'; import { AppRoomBridge } from './rooms'; +import { AppInternalBridge } from './internal'; import { AppSettingBridge } from './settings'; import { AppUserBridge } from './users'; @@ -18,13 +20,15 @@ export class RealAppBridges extends AppBridges { this._actBridge = new AppActivationBridge(orch); this._cmdBridge = new AppCommandsBridge(orch); + this._apiBridge = new AppApisBridge(orch); this._detBridge = new AppDetailChangesBridge(orch); this._envBridge = new AppEnvironmentalVariableBridge(orch); - this._httpBridge = new AppHttpBridge(); + this._httpBridge = new AppHttpBridge(orch); this._lisnBridge = new AppListenerBridge(orch); this._msgBridge = new AppMessageBridge(orch); this._persistBridge = new AppPersistenceBridge(orch); this._roomBridge = new AppRoomBridge(orch); + this._internalBridge = new AppInternalBridge(orch); this._setsBridge = new AppSettingBridge(orch); this._userBridge = new AppUserBridge(orch); } @@ -33,6 +37,10 @@ export class RealAppBridges extends AppBridges { return this._cmdBridge; } + getApiBridge() { + return this._apiBridge; + } + getEnvironmentalVariableBridge() { return this._envBridge; } @@ -65,6 +73,10 @@ export class RealAppBridges extends AppBridges { return this._roomBridge; } + getInternalBridge() { + return this._internalBridge; + } + getServerSettingBridge() { return this._setsBridge; } diff --git a/app/apps/server/bridges/commands.js b/app/apps/server/bridges/commands.js new file mode 100644 index 000000000000..827c2606d2c0 --- /dev/null +++ b/app/apps/server/bridges/commands.js @@ -0,0 +1,173 @@ +import { Meteor } from 'meteor/meteor'; +import { SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands'; + +import { slashCommands } from '../../../utils'; +import { Utilities } from '../../lib/misc/Utilities'; + +export class AppCommandsBridge { + constructor(orch) { + this.orch = orch; + this.disabledCommands = new Map(); + } + + doesCommandExist(command, appId) { + this.orch.debugLog(`The App ${ appId } is checking if "${ command }" command exists.`); + + if (typeof command !== 'string' || command.length === 0) { + return false; + } + + const cmd = command.toLowerCase(); + return typeof slashCommands.commands[cmd] === 'object' || this.disabledCommands.has(cmd); + } + + enableCommand(command, appId) { + this.orch.debugLog(`The App ${ appId } is attempting to enable the command: "${ command }"`); + + if (typeof command !== 'string' || command.trim().length === 0) { + throw new Error('Invalid command parameter provided, must be a string.'); + } + + const cmd = command.toLowerCase(); + if (!this.disabledCommands.has(cmd)) { + throw new Error(`The command is not currently disabled: "${ cmd }"`); + } + + slashCommands.commands[cmd] = this.disabledCommands.get(cmd); + this.disabledCommands.delete(cmd); + + this.orch.getNotifier().commandUpdated(cmd); + } + + disableCommand(command, appId) { + this.orch.debugLog(`The App ${ appId } is attempting to disable the command: "${ command }"`); + + if (typeof command !== 'string' || command.trim().length === 0) { + throw new Error('Invalid command parameter provided, must be a string.'); + } + + const cmd = command.toLowerCase(); + if (this.disabledCommands.has(cmd)) { + // The command is already disabled, no need to disable it yet again + return; + } + + if (typeof slashCommands.commands[cmd] === 'undefined') { + throw new Error(`Command does not exist in the system currently: "${ cmd }"`); + } + + this.disabledCommands.set(cmd, slashCommands.commands[cmd]); + delete slashCommands.commands[cmd]; + + this.orch.getNotifier().commandDisabled(cmd); + } + + // command: { command, paramsExample, i18nDescription, executor: function } + modifyCommand(command, appId) { + this.orch.debugLog(`The App ${ appId } is attempting to modify the command: "${ command }"`); + + this._verifyCommand(command); + + const cmd = command.toLowerCase(); + if (typeof slashCommands.commands[cmd] === 'undefined') { + throw new Error(`Command does not exist in the system currently (or it is disabled): "${ cmd }"`); + } + + const item = slashCommands.commands[cmd]; + item.params = command.paramsExample ? command.paramsExample : item.params; + item.description = command.i18nDescription ? command.i18nDescription : item.params; + item.callback = this._appCommandExecutor.bind(this); + item.providesPreview = command.providesPreview; + item.previewer = command.previewer ? this._appCommandPreviewer.bind(this) : item.previewer; + item.previewCallback = command.executePreviewItem ? this._appCommandPreviewExecutor.bind(this) : item.previewCallback; + + slashCommands.commands[cmd] = item; + this.orch.getNotifier().commandUpdated(cmd); + } + + registerCommand(command, appId) { + this.orch.debugLog(`The App ${ appId } is registering the command: "${ command.command }"`); + + this._verifyCommand(command); + + const item = { + command: command.command.toLowerCase(), + params: Utilities.getI18nKeyForApp(command.i18nParamsExample, appId), + description: Utilities.getI18nKeyForApp(command.i18nDescription, appId), + callback: this._appCommandExecutor.bind(this), + providesPreview: command.providesPreview, + previewer: !command.previewer ? undefined : this._appCommandPreviewer.bind(this), + previewCallback: !command.executePreviewItem ? undefined : this._appCommandPreviewExecutor.bind(this), + }; + + slashCommands.commands[command.command.toLowerCase()] = item; + this.orch.getNotifier().commandAdded(command.command.toLowerCase()); + } + + unregisterCommand(command, appId) { + this.orch.debugLog(`The App ${ appId } is unregistering the command: "${ command }"`); + + if (typeof command !== 'string' || command.trim().length === 0) { + throw new Error('Invalid command parameter provided, must be a string.'); + } + + const cmd = command.toLowerCase(); + this.disabledCommands.delete(cmd); + delete slashCommands.commands[cmd]; + + this.orch.getNotifier().commandRemoved(cmd); + } + + _verifyCommand(command) { + if (typeof command !== 'object') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + + if (typeof command.command !== 'string') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + + if (command.i18nParamsExample && typeof command.i18nParamsExample !== 'string') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + + if (command.i18nDescription && typeof command.i18nDescription !== 'string') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + + if (typeof command.providesPreview !== 'boolean') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + + if (typeof command.executor !== 'function') { + throw new Error('Invalid Slash Command parameter provided, it must be a valid ISlashCommand object.'); + } + } + + _appCommandExecutor(command, parameters, message) { + const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); + const room = this.orch.getConverters().get('rooms').convertById(message.rid); + const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params)); + Promise.await(this.orch.getManager().getCommandManager().executeCommand(command, context)); + } + + _appCommandPreviewer(command, parameters, message) { + const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); + const room = this.orch.getConverters().get('rooms').convertById(message.rid); + const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params)); + return Promise.await(this.orch.getManager().getCommandManager().getPreviews(command, context)); + } + + _appCommandPreviewExecutor(command, parameters, message, preview) { + const user = this.orch.getConverters().get('users').convertById(Meteor.userId()); + const room = this.orch.getConverters().get('rooms').convertById(message.rid); + const params = parameters.length === 0 || parameters === ' ' ? [] : parameters.split(' '); + + const context = new SlashCommandContext(Object.freeze(user), Object.freeze(room), Object.freeze(params)); + Promise.await(this.orch.getManager().getCommandManager().executePreview(command, preview, context)); + } +} diff --git a/packages/rocketchat-apps/server/bridges/details.js b/app/apps/server/bridges/details.js similarity index 100% rename from packages/rocketchat-apps/server/bridges/details.js rename to app/apps/server/bridges/details.js diff --git a/app/apps/server/bridges/environmental.js b/app/apps/server/bridges/environmental.js new file mode 100644 index 000000000000..28d8b3654eb8 --- /dev/null +++ b/app/apps/server/bridges/environmental.js @@ -0,0 +1,32 @@ +export class AppEnvironmentalVariableBridge { + constructor(orch) { + this.orch = orch; + this.allowed = ['NODE_ENV', 'ROOT_URL', 'INSTANCE_IP']; + } + + async getValueByName(envVarName, appId) { + this.orch.debugLog(`The App ${ appId } is getting the environmental variable value ${ envVarName }.`); + + if (!await this.isReadable(envVarName, appId)) { + throw new Error(`The environmental variable "${ envVarName }" is not readable.`); + } + + return process.env[envVarName]; + } + + async isReadable(envVarName, appId) { + this.orch.debugLog(`The App ${ appId } is checking if the environmental variable is readable ${ envVarName }.`); + + return this.allowed.includes(envVarName.toUpperCase()); + } + + async isSet(envVarName, appId) { + this.orch.debugLog(`The App ${ appId } is checking if the environmental variable is set ${ envVarName }.`); + + if (!await this.isReadable(envVarName, appId)) { + throw new Error(`The environmental variable "${ envVarName }" is not readable.`); + } + + return typeof process.env[envVarName] !== 'undefined'; + } +} diff --git a/app/apps/server/bridges/http.js b/app/apps/server/bridges/http.js new file mode 100644 index 000000000000..743343fa12c9 --- /dev/null +++ b/app/apps/server/bridges/http.js @@ -0,0 +1,21 @@ +import { HTTP } from 'meteor/http'; + +export class AppHttpBridge { + constructor(orch) { + this.orch = orch; + } + + async call(info) { + if (!info.request.content && typeof info.request.data === 'object') { + info.request.content = JSON.stringify(info.request.data); + } + + this.orch.debugLog(`The App ${ info.appId } is requesting from the outter webs:`, info); + + try { + return HTTP.call(info.method, info.url, info.request); + } catch (e) { + return e.response; + } + } +} diff --git a/app/apps/server/bridges/index.js b/app/apps/server/bridges/index.js new file mode 100644 index 000000000000..638c03c142c5 --- /dev/null +++ b/app/apps/server/bridges/index.js @@ -0,0 +1,27 @@ +import { RealAppBridges } from './bridges'; +import { AppActivationBridge } from './activation'; +import { AppCommandsBridge } from './commands'; +import { AppEnvironmentalVariableBridge } from './environmental'; +import { AppHttpBridge } from './http'; +import { AppListenerBridge } from './listeners'; +import { AppMessageBridge } from './messages'; +import { AppPersistenceBridge } from './persistence'; +import { AppRoomBridge } from './rooms'; +import { AppInternalBridge } from './internal'; +import { AppSettingBridge } from './settings'; +import { AppUserBridge } from './users'; + +export { + RealAppBridges, + AppActivationBridge, + AppCommandsBridge, + AppEnvironmentalVariableBridge, + AppHttpBridge, + AppListenerBridge, + AppMessageBridge, + AppPersistenceBridge, + AppRoomBridge, + AppSettingBridge, + AppUserBridge, + AppInternalBridge, +}; diff --git a/app/apps/server/bridges/internal.js b/app/apps/server/bridges/internal.js new file mode 100644 index 000000000000..ef883a68437f --- /dev/null +++ b/app/apps/server/bridges/internal.js @@ -0,0 +1,27 @@ +import { Subscriptions, Settings } from '../../../models'; + +export class AppInternalBridge { + constructor(orch) { + this.orch = orch; + } + + getUsernamesOfRoomById(roomId) { + const records = Subscriptions.findByRoomIdWhenUsernameExists(roomId, { + fields: { + 'u.username': 1, + }, + }).fetch(); + + if (!records || records.length === 0) { + return []; + } + + return records.map((s) => s.u.username); + } + + getWorkspacePublicKey() { + const publicKeySetting = Settings.findById('Cloud_Workspace_PublicKey').fetch()[0]; + + return this.orch.getConverters().get('settings').convertToApp(publicKeySetting); + } +} diff --git a/app/apps/server/bridges/listeners.js b/app/apps/server/bridges/listeners.js new file mode 100644 index 000000000000..26f0b7163089 --- /dev/null +++ b/app/apps/server/bridges/listeners.js @@ -0,0 +1,39 @@ +export class AppListenerBridge { + constructor(orch) { + this.orch = orch; + } + + async messageEvent(inte, message) { + const msg = this.orch.getConverters().get('messages').convertMessage(message); + const result = await this.orch.getManager().getListenerManager().executeListener(inte, msg); + + if (typeof result === 'boolean') { + return result; + } + return this.orch.getConverters().get('messages').convertAppMessage(result); + + // try { + + // } catch (e) { + // this.orch.debugLog(`${ e.name }: ${ e.message }`); + // this.orch.debugLog(e.stack); + // } + } + + async roomEvent(inte, room) { + const rm = this.orch.getConverters().get('rooms').convertRoom(room); + const result = await this.orch.getManager().getListenerManager().executeListener(inte, rm); + + if (typeof result === 'boolean') { + return result; + } + return this.orch.getConverters().get('rooms').convertAppRoom(result); + + // try { + + // } catch (e) { + // this.orch.debugLog(`${ e.name }: ${ e.message }`); + // this.orch.debugLog(e.stack); + // } + } +} diff --git a/app/apps/server/bridges/messages.js b/app/apps/server/bridges/messages.js new file mode 100644 index 000000000000..062efcd64b8a --- /dev/null +++ b/app/apps/server/bridges/messages.js @@ -0,0 +1,84 @@ +import { Meteor } from 'meteor/meteor'; +import { Random } from 'meteor/random'; + +import { Messages, Users, Subscriptions } from '../../../models'; +import { Notifications } from '../../../notifications'; +import { updateMessage } from '../../../lib/server/functions/updateMessage'; + +export class AppMessageBridge { + constructor(orch) { + this.orch = orch; + } + + async create(message, appId) { + this.orch.debugLog(`The App ${ appId } is creating a new message.`); + + let msg = this.orch.getConverters().get('messages').convertAppMessage(message); + + Meteor.runAsUser(msg.u._id, () => { + msg = Meteor.call('sendMessage', msg); + }); + + return msg._id; + } + + async getById(messageId, appId) { + this.orch.debugLog(`The App ${ appId } is getting the message: "${ messageId }"`); + + return this.orch.getConverters().get('messages').convertById(messageId); + } + + async update(message, appId) { + this.orch.debugLog(`The App ${ appId } is updating a message.`); + + if (!message.editor) { + throw new Error('Invalid editor assigned to the message for the update.'); + } + + if (!message.id || !Messages.findOneById(message.id)) { + throw new Error('A message must exist to update.'); + } + + const msg = this.orch.getConverters().get('messages').convertAppMessage(message); + const editor = Users.findOneById(message.editor.id); + + updateMessage(msg, editor); + } + + async notifyUser(user, message, appId) { + this.orch.debugLog(`The App ${ appId } is notifying a user.`); + + const msg = this.orch.getConverters().get('messages').convertAppMessage(message); + + Notifications.notifyUser(user.id, 'message', Object.assign(msg, { + _id: Random.id(), + ts: new Date(), + u: undefined, + editor: undefined, + })); + } + + async notifyRoom(room, message, appId) { + this.orch.debugLog(`The App ${ appId } is notifying a room's users.`); + + if (room) { + const msg = this.orch.getConverters().get('messages').convertAppMessage(message); + const rmsg = Object.assign(msg, { + _id: Random.id(), + rid: room.id, + ts: new Date(), + u: undefined, + editor: undefined, + }); + + const users = Subscriptions.findByRoomIdWhenUserIdExists(room._id, { fields: { 'u._id': 1 } }) + .fetch() + .map((s) => s.u._id); + Users.findByIds(users, { fields: { _id: 1 } }) + .fetch() + .forEach(({ _id }) => + Notifications.notifyUser(_id, 'message', rmsg) + ); + } + } +} diff --git a/app/apps/server/bridges/persistence.js b/app/apps/server/bridges/persistence.js new file mode 100644 index 000000000000..47a6f2639e23 --- /dev/null +++ b/app/apps/server/bridges/persistence.js @@ -0,0 +1,110 @@ +export class AppPersistenceBridge { + constructor(orch) { + this.orch = orch; + } + + async purge(appId) { + this.orch.debugLog(`The App's persistent storage is being purged: ${ appId }`); + + this.orch.getPersistenceModel().remove({ appId }); + } + + async create(data, appId) { + this.orch.debugLog(`The App ${ appId } is storing a new object in their persistence.`, data); + + if (typeof data !== 'object') { + throw new Error('Attempted to store an invalid data type, it must be an object.'); + } + + return this.orch.getPersistenceModel().insert({ appId, data }); + } + + async createWithAssociations(data, associations, appId) { + this.orch.debugLog(`The App ${ appId } is storing a new object in their persistence that is associated with some models.`, data, associations); + + if (typeof data !== 'object') { + throw new Error('Attempted to store an invalid data type, it must be an object.'); + } + + return this.orch.getPersistenceModel().insert({ appId, associations, data }); + } + + async readById(id, appId) { + this.orch.debugLog(`The App ${ appId } is reading their data in their persistence with the id: "${ id }"`); + + const record = this.orch.getPersistenceModel().findOneById(id); + + return record.data; + } + + async readByAssociations(associations, appId) { + this.orch.debugLog(`The App ${ appId } is searching for records that are associated with the following:`, associations); + + const records = this.orch.getPersistenceModel().find({ + appId, + associations: { $all: associations }, + }).fetch(); + + return Array.isArray(records) ? records.map((r) => r.data) : []; + } + + async remove(id, appId) { + this.orch.debugLog(`The App ${ appId } is removing one of their records by the id: "${ id }"`); + + const record = this.orch.getPersistenceModel().findOne({ _id: id, appId }); + + if (!record) { + return undefined; + } + + this.orch.getPersistenceModel().remove({ _id: id, appId }); + + return record.data; + } + + async removeByAssociations(associations, appId) { + this.orch.debugLog(`The App ${ appId } is removing records with the following associations:`, associations); + + const query = { + appId, + associations: { + $all: associations, + }, + }; + + const records = this.orch.getPersistenceModel().find(query).fetch(); + + if (!records) { + return undefined; + } + + this.orch.getPersistenceModel().remove(query); + + return Array.isArray(records) ? records.map((r) => r.data) : []; + } + + async update(id, data, upsert, appId) { + this.orch.debugLog(`The App ${ appId } is updating the record "${ id }" to:`, data); + + if (typeof data !== 'object') { + throw new Error('Attempted to store an invalid data type, it must be an object.'); + } + + throw new Error('Not implemented.'); + } + + async updateByAssociations(associations, data, upsert, appId) { + this.orch.debugLog(`The App ${ appId } is updating the record with association to data as follows:`, associations, data); + + if (typeof data !== 'object') { + throw new Error('Attempted to store an invalid data type, it must be an object.'); + } + + const query = { + appId, + associations, + }; + + return this.orch.getPersistenceModel().upsert(query, { $set: { data } }, { upsert }); + } +} diff --git a/app/apps/server/bridges/rooms.js b/app/apps/server/bridges/rooms.js new file mode 100644 index 000000000000..250f25aa22c3 --- /dev/null +++ b/app/apps/server/bridges/rooms.js @@ -0,0 +1,124 @@ +import { Meteor } from 'meteor/meteor'; +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { Rooms, Subscriptions, Users } from '../../../models'; +import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; + +export class AppRoomBridge { + constructor(orch) { + this.orch = orch; + } + + async create(room, members, appId) { + this.orch.debugLog(`The App ${ appId } is creating a new room.`, room); + + const rcRoom = this.orch.getConverters().get('rooms').convertAppRoom(room); + let method; + + switch (room.type) { + case RoomType.CHANNEL: + method = 'createChannel'; + break; + case RoomType.PRIVATE_GROUP: + method = 'createPrivateGroup'; + break; + case RoomType.DIRECT_MESSAGE: + method = 'createDirectMessage'; + break; + default: + throw new Error('Only channels, private groups and direct messages can be created.'); + } + + let rid; + Meteor.runAsUser(room.creator.id, () => { + const extraData = Object.assign({}, rcRoom); + delete extraData.name; + delete extraData.t; + delete extraData.ro; + delete extraData.customFields; + let info; + if (room.type === RoomType.DIRECT_MESSAGE) { + members.splice(members.indexOf(room.creator.username), 1); + info = Meteor.call(method, members[0]); + } else { + info = Meteor.call(method, rcRoom.name, members, rcRoom.ro, rcRoom.customFields, extraData); + } + rid = info.rid; + }); + + return rid; + } + + async getById(roomId, appId) { + this.orch.debugLog(`The App ${ appId } is getting the roomById: "${ roomId }"`); + + return this.orch.getConverters().get('rooms').convertById(roomId); + } + + async getByName(roomName, appId) { + this.orch.debugLog(`The App ${ appId } is getting the roomByName: "${ roomName }"`); + + return this.orch.getConverters().get('rooms').convertByName(roomName); + } + + async getCreatorById(roomId, appId) { + this.orch.debugLog(`The App ${ appId } is getting the room's creator by id: "${ roomId }"`); + + const room = Rooms.findOneById(roomId); + + if (!room || !room.u || !room.u._id) { + return undefined; + } + + return this.orch.getConverters().get('users').convertById(room.u._id); + } + + async getCreatorByName(roomName, appId) { + this.orch.debugLog(`The App ${ appId } is getting the room's creator by name: "${ roomName }"`); + + const room = Rooms.findOneByName(roomName); + + if (!room || !room.u || !room.u._id) { + return undefined; + } + + return this.orch.getConverters().get('users').convertById(room.u._id); + } + + async getMembers(roomId, appId) { + this.orch.debugLog(`The App ${ appId } is getting the room's members by room id: "${ roomId }"`); + const subscriptions = await Subscriptions.findByRoomId(roomId); + return subscriptions.map((sub) => this.orch.getConverters().get('users').convertById(sub.u && sub.u._id)); + } + + async getDirectByUsernames(usernames, appId) { + this.orch.debugLog(`The App ${ appId } is getting direct room by usernames: "${ usernames }"`); + const room = await Rooms.findDirectRoomContainingAllUsernames(usernames); + if (!room) { + return undefined; + } + return this.orch.getConverters().get('rooms').convertRoom(room); + } + + async update(room, members = [], appId) { + this.orch.debugLog(`The App ${ appId } is updating a room.`); + + if (!room.id || !Rooms.findOneById(room.id)) { + throw new Error('A room must exist to update.'); + } + + const rm = this.orch.getConverters().get('rooms').convertAppRoom(room); + + Rooms.update(rm._id, rm); + + for (const username of members) { + const member = Users.findOneByUsername(username); + + if (!member) { + continue; + } + + addUserToRoom(rm._id, member); + } + } +} diff --git a/app/apps/server/bridges/settings.js b/app/apps/server/bridges/settings.js new file mode 100644 index 000000000000..ca01afacbbb7 --- /dev/null +++ b/app/apps/server/bridges/settings.js @@ -0,0 +1,58 @@ +import { Settings } from '../../../models'; + +export class AppSettingBridge { + constructor(orch) { + this.orch = orch; + this.allowedGroups = []; + } + + async getAll(appId) { + this.orch.debugLog(`The App ${ appId } is getting all the settings.`); + + return Settings.find({ secret: false }) + .fetch() + .map((s) => this.orch.getConverters().get('settings').convertToApp(s)); + } + + async getOneById(id, appId) { + this.orch.debugLog(`The App ${ appId } is getting the setting by id ${ id }.`); + + if (!this.isReadableById(id, appId)) { + throw new Error(`The setting "${ id }" is not readable.`); + } + + return this.orch.getConverters().get('settings').convertById(id); + } + + async hideGroup(name, appId) { + this.orch.debugLog(`The App ${ appId } is hidding the group ${ name }.`); + + throw new Error('Method not implemented.'); + } + + async hideSetting(id, appId) { + this.orch.debugLog(`The App ${ appId } is hidding the setting ${ id }.`); + + if (!this.isReadableById(id, appId)) { + throw new Error(`The setting "${ id }" is not readable.`); + } + + throw new Error('Method not implemented.'); + } + + async isReadableById(id, appId) { + this.orch.debugLog(`The App ${ appId } is checking if they can read the setting ${ id }.`); + + return !Settings.findOneById(id).secret; + } + + async updateOne(setting, appId) { + this.orch.debugLog(`The App ${ appId } is updating the setting ${ setting.id } .`); + + if (!this.isReadableById(setting.id, appId)) { + throw new Error(`The setting "${ setting.id }" is not readable.`); + } + + throw new Error('Method not implemented.'); + } +} diff --git a/app/apps/server/bridges/users.js b/app/apps/server/bridges/users.js new file mode 100644 index 000000000000..7329777f7026 --- /dev/null +++ b/app/apps/server/bridges/users.js @@ -0,0 +1,23 @@ +import { Users } from '../../../models/server'; + +export class AppUserBridge { + constructor(orch) { + this.orch = orch; + } + + async getById(userId, appId) { + this.orch.debugLog(`The App ${ appId } is getting the userId: "${ userId }"`); + + return this.orch.getConverters().get('users').convertById(userId); + } + + async getByUsername(username, appId) { + this.orch.debugLog(`The App ${ appId } is getting the username: "${ username }"`); + + return this.orch.getConverters().get('users').convertByUsername(username); + } + + async getActiveUserCount() { + return Users.getActiveLocalUserCount(); + } +} diff --git a/packages/rocketchat-apps/server/communication/index.js b/app/apps/server/communication/index.js similarity index 100% rename from packages/rocketchat-apps/server/communication/index.js rename to app/apps/server/communication/index.js diff --git a/app/apps/server/communication/methods.js b/app/apps/server/communication/methods.js new file mode 100644 index 000000000000..16170105caa6 --- /dev/null +++ b/app/apps/server/communication/methods.js @@ -0,0 +1,94 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../../settings'; +import { hasPermission } from '../../../authorization'; + +const waitToLoad = function(orch) { + return new Promise((resolve) => { + let id = setInterval(() => { + if (orch.isEnabled() && orch.isLoaded()) { + clearInterval(id); + id = -1; + resolve(); + } + }, 100); + }); +}; + +const waitToUnload = function(orch) { + return new Promise((resolve) => { + let id = setInterval(() => { + if (!orch.isEnabled() && !orch.isLoaded()) { + clearInterval(id); + id = -1; + resolve(); + } + }, 100); + }); +}; + +export class AppMethods { + constructor(orch) { + this._orch = orch; + + this._addMethods(); + } + + isEnabled() { + return typeof this._orch !== 'undefined' && this._orch.isEnabled(); + } + + isLoaded() { + return typeof this._orch !== 'undefined' && this._orch.isEnabled() && this._orch.isLoaded(); + } + + _addMethods() { + const instance = this; + + Meteor.methods({ + 'apps/is-enabled'() { + return instance.isEnabled(); + }, + + 'apps/is-loaded'() { + return instance.isLoaded(); + }, + + 'apps/go-enable'() { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'apps/go-enable', + }); + } + + if (!hasPermission(Meteor.userId(), 'manage-apps')) { + throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { + method: 'apps/go-enable', + }); + } + + settings.set('Apps_Framework_enabled', true); + + Promise.await(waitToLoad(instance._orch)); + }, + + 'apps/go-disable'() { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'apps/go-enable', + }); + } + + if (!hasPermission(Meteor.userId(), 'manage-apps')) { + throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { + method: 'apps/go-enable', + }); + } + + settings.set('Apps_Framework_enabled', false); + + Promise.await(waitToUnload(instance._orch)); + }, + }); + } +} diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js new file mode 100644 index 000000000000..5b0a4f19d40b --- /dev/null +++ b/app/apps/server/communication/rest.js @@ -0,0 +1,667 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; +import Busboy from 'busboy'; + +import { API } from '../../../api/server'; +import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server'; +import { settings } from '../../../settings'; +import { Info } from '../../../utils'; +import { Settings, Users } from '../../../models/server'; +import { Apps } from '../orchestrator'; + +const getDefaultHeaders = () => ({ + 'X-Apps-Engine-Version': Info.marketplaceApiVersion, +}); + +const purchaseTypes = new Set(['buy', 'subscription']); + +export class AppsRestApi { + constructor(orch, manager) { + this._orch = orch; + this._manager = manager; + this.loadAPI(); + } + + _handleFile(request, fileField) { + const busboy = new Busboy({ headers: request.headers }); + + return Meteor.wrapAsync((callback) => { + busboy.on('file', Meteor.bindEnvironment((fieldname, file) => { + if (fieldname !== fileField) { + return callback(new Meteor.Error('invalid-field', `Expected the field "${ fileField }" but got "${ fieldname }" instead.`)); + } + + const fileData = []; + file.on('data', Meteor.bindEnvironment((data) => { + fileData.push(data); + })); + + file.on('end', Meteor.bindEnvironment(() => callback(undefined, Buffer.concat(fileData)))); + })); + + request.pipe(busboy); + })(); + } + + async loadAPI() { + this.api = new API.ApiClass({ + version: 'apps', + useDefaultAuth: true, + prettyJson: false, + enableCors: false, + auth: API.getUserAuth(), + }); + this.addManagementRoutes(); + } + + addManagementRoutes() { + const orchestrator = this._orch; + const manager = this._manager; + const fileHandler = this._handleFile; + + this.api.addRoute('', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + + // Gets the Apps from the marketplace + if (this.queryParams.marketplace) { + const headers = getDefaultHeaders(); + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/apps`, { + headers, + }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting the Apps:', e.response.data); + return API.v1.internalError(); + } + + if (!result || result.statusCode !== 200) { + orchestrator.getRocketChatLogger().error('Error getting the Apps:', result.data); + return API.v1.failure(); + } + + return API.v1.success(result.data); + } + + if (this.queryParams.categories) { + const headers = getDefaultHeaders(); + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/categories`, { + headers, + }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', e.response.data); + return API.v1.internalError(); + } + + if (!result || result.statusCode !== 200) { + orchestrator.getRocketChatLogger().error('Error getting the categories from the Marketplace:', result.data); + return API.v1.failure(); + } + + return API.v1.success(result.data); + } + + if (this.queryParams.buildExternalUrl && this.queryParams.appId) { + const workspaceId = settings.get('Cloud_Workspace_Id'); + + if (!this.queryParams.purchaseType || !purchaseTypes.has(this.queryParams.purchaseType)) { + return API.v1.failure({ error: 'Invalid purchase type' }); + } + + const token = getUserCloudAccessToken(this.getLoggedInUser()._id, true, 'marketplace:purchase', false); + if (!token) { + return API.v1.failure({ error: 'Unauthorized' }); + } + + const subscribeRoute = this.queryParams.details === 'true' ? 'subscribe/details' : 'subscribe'; + + const seats = Users.getActiveLocalUserCount(); + + return API.v1.success({ + url: `${ baseUrl }/apps/${ this.queryParams.appId }/${ + this.queryParams.purchaseType === 'buy' ? this.queryParams.purchaseType : subscribeRoute + }?workspaceId=${ workspaceId }&token=${ token }&seats=${ seats }`, + }); + } + + const apps = manager.get().map((prl) => { + const info = prl.getInfo(); + info.languages = prl.getStorageItem().languageContent; + info.status = prl.getStatus(); + + return info; + }); + + return API.v1.success({ apps }); + }, + post() { + let buff; + let marketplaceInfo; + + if (this.bodyParams.url) { + if (settings.get('Apps_Framework_Development_Mode') !== true) { + return API.v1.failure({ error: 'Installation from url is disabled.' }); + } + + let result; + try { + result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: null } }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting the app from url:', e.response.data); + return API.v1.internalError(); + } + + if (result.statusCode !== 200 || !result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') { + return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' }); + } + + buff = result.content; + } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = getDefaultHeaders(); + + const downloadPromise = new Promise((resolve, reject) => { + const token = getWorkspaceAccessToken(true, 'marketplace:download', false); + + HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }?token=${ token }`, { + headers, + npmRequestOptions: { encoding: null }, + }, (error, result) => { + if (error) { reject(error); } + + resolve(result); + }); + }); + + const marketplacePromise = new Promise((resolve, reject) => { + const token = getWorkspaceAccessToken(); + + HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }?appVersion=${ this.bodyParams.version }`, { + headers: { + Authorization: `Bearer ${ token }`, + ...headers, + }, + }, (error, result) => { + if (error) { reject(error); } + + resolve(result); + }); + }); + + + try { + const [downloadResult, marketplaceResult] = Promise.await(Promise.all([downloadPromise, marketplacePromise])); + + if (!downloadResult.headers['content-type'] || downloadResult.headers['content-type'] !== 'application/zip') { + throw new Error('Invalid url. It doesn\'t exist or is not "application/zip".'); + } + + buff = downloadResult.content; + marketplaceInfo = marketplaceResult.data[0]; + } catch (err) { + return API.v1.failure(err.message); + } + } else { + if (settings.get('Apps_Framework_Development_Mode') !== true) { + return API.v1.failure({ error: 'Direct installation of an App is disabled.' }); + } + + buff = fileHandler(this.request, 'app'); + } + + if (!buff) { + return API.v1.failure({ error: 'Failed to get a file to install for the App. ' }); + } + + const aff = Promise.await(manager.add(buff.toString('base64'), false, marketplaceInfo)); + const info = aff.getAppInfo(); + + if (aff.hasStorageError()) { + return API.v1.failure({ status: 'storage_error', messages: [aff.getStorageError()] }); + } + + if (aff.getCompilerErrors().length) { + return API.v1.failure({ status: 'compiler_error', messages: aff.getCompilerErrors() }); + } + + if (aff.getLicenseValidationResult().hasErrors) { + return API.v1.failure({ status: 'license_error', messages: aff.getLicenseValidationResult().getErrors() }); + } + + info.status = aff.getApp().getStatus(); + + return API.v1.success({ + app: info, + implemented: aff.getImplementedInferfaces(), + licenseValidation: aff.getLicenseValidationResult(), + }); + }, + }); + + this.api.addRoute('languages', { authRequired: false }, { + get() { + const apps = manager.get().map((prl) => ({ + id: prl.getID(), + languages: prl.getStorageItem().languageContent, + })); + + return API.v1.success({ apps }); + }, + }); + + this.api.addRoute('bundles/:id/apps', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = {}; + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/bundles/${ this.urlParams.id }/apps`, { + headers, + }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting the Bundle\'s Apps from the Marketplace:', e.response.data); + return API.v1.internalError(); + } + + if (!result || result.statusCode !== 200 || result.data.length === 0) { + orchestrator.getRocketChatLogger().error('Error getting the Bundle\'s Apps from the Marketplace:', result.data); + return API.v1.failure(); + } + + return API.v1.success({ apps: result.data }); + }, + }); + + const handleError = (message, e) => { + orchestrator.getRocketChatLogger().error(message, e.response.data); + + if (e.response.statusCode >= 500 && e.response.statusCode <= 599) { + return API.v1.internalError(); + } + + if (e.response.statusCode === 404) { + return API.v1.notFound(); + } + + return API.v1.failure(); + }; + + this.api.addRoute(':id', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + if (this.queryParams.marketplace && this.queryParams.version) { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }?appVersion=${ this.queryParams.version }`, { + headers, + }); + } catch (e) { + return handleError('Error getting the App information from the Marketplace:', e); + } + + if (!result || result.statusCode !== 200 || result.data.length === 0) { + orchestrator.getRocketChatLogger().error('Error getting the App information from the Marketplace:', result.data); + return API.v1.failure(); + } + + return API.v1.success({ app: result.data[0] }); + } + + if (this.queryParams.marketplace && this.queryParams.update && this.queryParams.appVersion) { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = getDefaultHeaders(); + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/apps/${ this.urlParams.id }/latest?frameworkVersion=${ Info.marketplaceApiVersion }`, { + headers, + }); + } catch (e) { + return handleError('Error getting the App update info from the Marketplace:', e); + } + + if (result.statusCode !== 200 || result.data.length === 0) { + orchestrator.getRocketChatLogger().error('Error getting the App update info from the Marketplace:', result.data); + return API.v1.failure(); + } + + return API.v1.success({ app: result.data }); + } + + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + const info = prl.getInfo(); + + return API.v1.success({ + app: { + ...info, + status: prl.getStatus(), + licenseValidation: prl.getLatestLicenseValidationResult(), + }, + }); + } + + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + post() { + let buff; + + if (this.bodyParams.url) { + if (settings.get('Apps_Framework_Development_Mode') !== true) { + return API.v1.failure({ error: 'Updating an App from a url is disabled.' }); + } + + const result = HTTP.call('GET', this.bodyParams.url, { npmRequestOptions: { encoding: null } }); + + if (result.statusCode !== 200 || !result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') { + return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' }); + } + + buff = result.content; + } else if (this.bodyParams.appId && this.bodyParams.marketplace && this.bodyParams.version) { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = getDefaultHeaders(); + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }/download/${ this.bodyParams.version }`, { + headers, + npmRequestOptions: { encoding: null }, + }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', e.response.data); + return API.v1.internalError(); + } + + if (result.statusCode !== 200) { + orchestrator.getRocketChatLogger().error('Error getting the App from the Marketplace:', result.data); + return API.v1.failure(); + } + + if (!result.headers['content-type'] || result.headers['content-type'] !== 'application/zip') { + return API.v1.failure({ error: 'Invalid url. It doesn\'t exist or is not "application/zip".' }); + } + + buff = result.content; + } else { + if (settings.get('Apps_Framework_Development_Mode') !== true) { + return API.v1.failure({ error: 'Direct updating of an App is disabled.' }); + } + + buff = fileHandler(this.request, 'app'); + } + + if (!buff) { + return API.v1.failure({ error: 'Failed to get a file to install for the App. ' }); + } + + const aff = Promise.await(manager.update(buff.toString('base64'))); + const info = aff.getAppInfo(); + + // Should the updated version have compiler errors, no App will be returned + if (aff.getApp()) { + info.status = aff.getApp().getStatus(); + } else { + info.status = 'compiler_error'; + } + + return API.v1.success({ + app: info, + implemented: aff.getImplementedInferfaces(), + compilerErrors: aff.getCompilerErrors(), + }); + }, + delete() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + Promise.await(manager.remove(prl.getID())); + + const info = prl.getInfo(); + info.status = prl.getStatus(); + + return API.v1.success({ app: info }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + }); + + this.api.addRoute(':id/sync', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + post() { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = getDefaultHeaders(); + const token = getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${ token }`; + } + + const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch(); + + let result; + try { + result = HTTP.get(`${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps/${ this.urlParams.id }`, { + headers, + }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', e.response.data); + return API.v1.internalError(); + } + + if (result.statusCode !== 200) { + orchestrator.getRocketChatLogger().error('Error syncing the App from the Marketplace:', result.data); + return API.v1.failure(); + } + + Promise.await(Apps.updateAppsMarketplaceInfo([result.data])); + + return API.v1.success({ app: result.data }); + }, + }); + + this.api.addRoute(':id/icon', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + const info = prl.getInfo(); + + return API.v1.success({ iconFileContent: info.iconFileContent }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + }); + + this.api.addRoute(':id/languages', { authRequired: false }, { + get() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + const languages = prl.getStorageItem().languageContent || {}; + + return API.v1.success({ languages }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + }); + + this.api.addRoute(':id/logs', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + const { offset, count } = this.getPaginationItems(); + const { sort, fields, query } = this.parseJsonQuery(); + + const ourQuery = Object.assign({}, query, { appId: prl.getID() }); + const options = { + sort: sort || { _updatedAt: -1 }, + skip: offset, + limit: count, + fields, + }; + + const logs = Promise.await(orchestrator.getLogStorage().find(ourQuery, options)); + + return API.v1.success({ logs }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + }); + + this.api.addRoute(':id/settings', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + const settings = Object.assign({}, prl.getStorageItem().settings); + + Object.keys(settings).forEach((k) => { + if (settings[k].hidden) { + delete settings[k]; + } + }); + + return API.v1.success({ settings }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + post() { + if (!this.bodyParams || !this.bodyParams.settings) { + return API.v1.failure('The settings to update must be present.'); + } + + const prl = manager.getOneById(this.urlParams.id); + + if (!prl) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + } + + const { settings } = prl.getStorageItem(); + + const updated = []; + this.bodyParams.settings.forEach((s) => { + if (settings[s.id]) { + Promise.await(manager.getSettingsManager().updateAppSetting(this.urlParams.id, s)); + // Updating? + updated.push(s); + } + }); + + return API.v1.success({ updated }); + }, + }); + + this.api.addRoute(':id/settings/:settingId', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + try { + const setting = manager.getSettingsManager().getAppSetting(this.urlParams.id, this.urlParams.settingId); + + API.v1.success({ setting }); + } catch (e) { + if (e.message.includes('No setting found')) { + return API.v1.notFound(`No Setting found on the App by the id of: "${ this.urlParams.settingId }"`); + } if (e.message.includes('No App found')) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + } + return API.v1.failure(e.message); + } + }, + post() { + if (!this.bodyParams.setting) { + return API.v1.failure('Setting to update to must be present on the posted body.'); + } + + try { + Promise.await(manager.getSettingsManager().updateAppSetting(this.urlParams.id, this.bodyParams.setting)); + + return API.v1.success(); + } catch (e) { + if (e.message.includes('No setting found')) { + return API.v1.notFound(`No Setting found on the App by the id of: "${ this.urlParams.settingId }"`); + } if (e.message.includes('No App found')) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + } + return API.v1.failure(e.message); + } + }, + }); + + this.api.addRoute(':id/apis', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + return API.v1.success({ + apis: manager.apiManager.listApis(this.urlParams.id), + }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + }); + + this.api.addRoute(':id/status', { authRequired: true, permissionsRequired: ['manage-apps'] }, { + get() { + const prl = manager.getOneById(this.urlParams.id); + + if (prl) { + return API.v1.success({ status: prl.getStatus() }); + } + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + }, + post() { + if (!this.bodyParams.status || typeof this.bodyParams.status !== 'string') { + return API.v1.failure('Invalid status provided, it must be "status" field and a string.'); + } + + const prl = manager.getOneById(this.urlParams.id); + + if (!prl) { + return API.v1.notFound(`No App found by the id of: ${ this.urlParams.id }`); + } + + const result = Promise.await(manager.changeStatus(prl.getID(), this.bodyParams.status)); + + return API.v1.success({ status: result.getStatus() }); + }, + }); + } +} diff --git a/app/apps/server/communication/websockets.js b/app/apps/server/communication/websockets.js new file mode 100644 index 000000000000..857158ddd3bb --- /dev/null +++ b/app/apps/server/communication/websockets.js @@ -0,0 +1,183 @@ +import { Meteor } from 'meteor/meteor'; +import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; + +export const AppEvents = Object.freeze({ + APP_ADDED: 'app/added', + APP_REMOVED: 'app/removed', + APP_UPDATED: 'app/updated', + APP_STATUS_CHANGE: 'app/statusUpdate', + APP_SETTING_UPDATED: 'app/settingUpdated', + COMMAND_ADDED: 'command/added', + COMMAND_DISABLED: 'command/disabled', + COMMAND_UPDATED: 'command/updated', + COMMAND_REMOVED: 'command/removed', +}); + +export class AppServerListener { + constructor(orch, engineStreamer, clientStreamer, received) { + this.orch = orch; + this.engineStreamer = engineStreamer; + this.clientStreamer = clientStreamer; + this.received = received; + + this.engineStreamer.on(AppEvents.APP_STATUS_CHANGE, this.onAppStatusUpdated.bind(this)); + this.engineStreamer.on(AppEvents.APP_REMOVED, this.onAppRemoved.bind(this)); + this.engineStreamer.on(AppEvents.APP_UPDATED, this.onAppUpdated.bind(this)); + this.engineStreamer.on(AppEvents.APP_ADDED, this.onAppAdded.bind(this)); + + this.engineStreamer.on(AppEvents.APP_SETTING_UPDATED, this.onAppSettingUpdated.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_ADDED, this.onCommandAdded.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_DISABLED, this.onCommandDisabled.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_UPDATED, this.onCommandUpdated.bind(this)); + this.engineStreamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemoved.bind(this)); + } + + async onAppAdded(appId) { + await this.orch.getManager().loadOne(appId); + this.clientStreamer.emit(AppEvents.APP_ADDED, appId); + } + + + async onAppStatusUpdated({ appId, status }) { + const app = this.orch.getManager().getOneById(appId); + + if (app.getStatus() === status) { + return; + } + + this.received.set(`${ AppEvents.APP_STATUS_CHANGE }_${ appId }`, { appId, status, when: new Date() }); + + if (AppStatusUtils.isEnabled(status)) { + await this.orch.getManager().enable(appId).catch(console.error); + this.clientStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status }); + } else if (AppStatusUtils.isDisabled(status)) { + await this.orch.getManager().disable(appId, status, true).catch(console.error); + this.clientStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status }); + } + } + + async onAppSettingUpdated({ appId, setting }) { + this.received.set(`${ AppEvents.APP_SETTING_UPDATED }_${ appId }_${ setting.id }`, { appId, setting, when: new Date() }); + await this.orch.getManager().getSettingsManager().updateAppSetting(appId, setting); + this.clientStreamer.emit(AppEvents.APP_SETTING_UPDATED, { appId }); + } + + async onAppUpdated(appId) { + this.received.set(`${ AppEvents.APP_UPDATED }_${ appId }`, { appId, when: new Date() }); + + const storageItem = await this.orch.getStorage().retrieveOne(appId); + + await this.orch.getManager().update(storageItem.zip); + this.clientStreamer.emit(AppEvents.APP_UPDATED, appId); + } + + async onAppRemoved(appId) { + const app = this.orch.getManager().getOneById(appId); + + if (!app) { + return; + } + + await this.orch.getManager().remove(appId); + this.clientStreamer.emit(AppEvents.APP_REMOVED, appId); + } + + async onCommandAdded(command) { + this.clientStreamer.emit(AppEvents.COMMAND_ADDED, command); + } + + async onCommandDisabled(command) { + this.clientStreamer.emit(AppEvents.COMMAND_DISABLED, command); + } + + async onCommandUpdated(command) { + this.clientStreamer.emit(AppEvents.COMMAND_UPDATED, command); + } + + async onCommandRemoved(command) { + this.clientStreamer.emit(AppEvents.COMMAND_REMOVED, command); + } +} + +export class AppServerNotifier { + constructor(orch) { + this.engineStreamer = new Meteor.Streamer('apps-engine', { retransmit: false }); + this.engineStreamer.serverOnly = true; + this.engineStreamer.allowRead('none'); + this.engineStreamer.allowEmit('all'); + this.engineStreamer.allowWrite('none'); + + // This is used to broadcast to the web clients + this.clientStreamer = new Meteor.Streamer('apps', { retransmit: false }); + this.clientStreamer.serverOnly = true; + this.clientStreamer.allowRead('all'); + this.clientStreamer.allowEmit('all'); + this.clientStreamer.allowWrite('none'); + + this.received = new Map(); + this.listener = new AppServerListener(orch, this.engineStreamer, this.clientStreamer, this.received); + } + + async appAdded(appId) { + this.engineStreamer.emit(AppEvents.APP_ADDED, appId); + this.clientStreamer.emit(AppEvents.APP_ADDED, appId); + } + + async appRemoved(appId) { + this.engineStreamer.emit(AppEvents.APP_REMOVED, appId); + this.clientStreamer.emit(AppEvents.APP_REMOVED, appId); + } + + async appUpdated(appId) { + if (this.received.has(`${ AppEvents.APP_UPDATED }_${ appId }`)) { + this.received.delete(`${ AppEvents.APP_UPDATED }_${ appId }`); + return; + } + + this.engineStreamer.emit(AppEvents.APP_UPDATED, appId); + this.clientStreamer.emit(AppEvents.APP_UPDATED, appId); + } + + async appStatusUpdated(appId, status) { + if (this.received.has(`${ AppEvents.APP_STATUS_CHANGE }_${ appId }`)) { + const details = this.received.get(`${ AppEvents.APP_STATUS_CHANGE }_${ appId }`); + if (details.status === status) { + this.received.delete(`${ AppEvents.APP_STATUS_CHANGE }_${ appId }`); + return; + } + } + + this.engineStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status }); + this.clientStreamer.emit(AppEvents.APP_STATUS_CHANGE, { appId, status }); + } + + async appSettingsChange(appId, setting) { + if (this.received.has(`${ AppEvents.APP_SETTING_UPDATED }_${ appId }_${ setting.id }`)) { + this.received.delete(`${ AppEvents.APP_SETTING_UPDATED }_${ appId }_${ setting.id }`); + return; + } + + this.engineStreamer.emit(AppEvents.APP_SETTING_UPDATED, { appId, setting }); + this.clientStreamer.emit(AppEvents.APP_SETTING_UPDATED, { appId }); + } + + async commandAdded(command) { + this.engineStreamer.emit(AppEvents.COMMAND_ADDED, command); + this.clientStreamer.emit(AppEvents.COMMAND_ADDED, command); + } + + async commandDisabled(command) { + this.engineStreamer.emit(AppEvents.COMMAND_DISABLED, command); + this.clientStreamer.emit(AppEvents.COMMAND_DISABLED, command); + } + + async commandUpdated(command) { + this.engineStreamer.emit(AppEvents.COMMAND_UPDATED, command); + this.clientStreamer.emit(AppEvents.COMMAND_UPDATED, command); + } + + async commandRemoved(command) { + this.engineStreamer.emit(AppEvents.COMMAND_REMOVED, command); + this.clientStreamer.emit(AppEvents.COMMAND_REMOVED, command); + } +} diff --git a/packages/rocketchat-apps/server/converters/index.js b/app/apps/server/converters/index.js similarity index 100% rename from packages/rocketchat-apps/server/converters/index.js rename to app/apps/server/converters/index.js diff --git a/app/apps/server/converters/messages.js b/app/apps/server/converters/messages.js new file mode 100644 index 000000000000..2abaedd58b92 --- /dev/null +++ b/app/apps/server/converters/messages.js @@ -0,0 +1,238 @@ +import { Random } from 'meteor/random'; + +import { Messages, Rooms, Users } from '../../../models'; +import { transformMappedData } from '../../lib/misc/transformMappedData'; + +export class AppMessagesConverter { + constructor(orch) { + this.orch = orch; + } + + convertById(msgId) { + const msg = Messages.findOneById(msgId); + + return this.convertMessage(msg); + } + + convertMessage(msgObj) { + if (!msgObj) { + return undefined; + } + + const map = { + id: '_id', + reactions: 'reactions', + parseUrls: 'parseUrls', + text: 'msg', + createdAt: 'ts', + updatedAt: '_updatedAt', + editedAt: 'editedAt', + emoji: 'emoji', + avatarUrl: 'avatar', + alias: 'alias', + customFields: 'customFields', + groupable: 'groupable', + room: (message) => { + const result = this.orch.getConverters().get('rooms').convertById(message.rid); + delete message.rid; + return result; + }, + editor: (message) => { + const { editedBy } = message; + delete message.editedBy; + + if (!editedBy) { + return undefined; + } + + return this.orch.getConverters().get('users').convertById(editedBy._id); + }, + attachments: (message) => { + const result = this._convertAttachmentsToApp(message.attachments); + delete message.attachments; + return result; + }, + sender: (message) => { + if (!message.u || !message.u._id) { + return undefined; + } + + let user = this.orch.getConverters().get('users').convertById(message.u._id); + + // When the sender of the message is a Guest (livechat) and not a user + if (!user) { + user = this.orch.getConverters().get('users').convertToApp(message.u); + } + + delete message.u; + + return user; + }, + }; + + return transformMappedData(msgObj, map); + } + + convertAppMessage(message) { + if (!message) { + return undefined; + } + + const room = Rooms.findOneById(message.room.id); + + if (!room) { + throw new Error('Invalid room provided on the message.'); + } + + let u; + if (message.sender && message.sender.id) { + const user = Users.findOneById(message.sender.id); + + if (user) { + u = { + _id: user._id, + username: user.username, + name: user.name, + }; + } else { + u = { + _id: message.sender.id, + username: message.sender.username, + name: message.sender.name, + }; + } + } + + let editedBy; + if (message.editor) { + const editor = Users.findOneById(message.editor.id); + editedBy = { + _id: editor._id, + username: editor.username, + }; + } + + const attachments = this._convertAppAttachments(message.attachments); + + const newMessage = { + _id: message.id || Random.id(), + rid: room._id, + u, + msg: message.text, + ts: message.createdAt || new Date(), + _updatedAt: message.updatedAt || new Date(), + editedBy, + editedAt: message.editedAt, + emoji: message.emoji, + avatar: message.avatarUrl, + alias: message.alias, + customFields: message.customFields, + groupable: message.groupable, + attachments, + reactions: message.reactions, + parseUrls: message.parseUrls, + }; + + return Object.assign(newMessage, message._unmappedProperties_); + } + + _convertAppAttachments(attachments) { + if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { + return undefined; + } + + return attachments.map((attachment) => Object.assign({ + collapsed: attachment.collapsed, + color: attachment.color, + text: attachment.text, + ts: attachment.timestamp ? attachment.timestamp.toJSON() : attachment.timestamp, + message_link: attachment.timestampLink, + thumb_url: attachment.thumbnailUrl, + author_name: attachment.author ? attachment.author.name : undefined, + author_link: attachment.author ? attachment.author.link : undefined, + author_icon: attachment.author ? attachment.author.icon : undefined, + title: attachment.title ? attachment.title.value : undefined, + title_link: attachment.title ? attachment.title.link : undefined, + title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, + image_dimensions: attachment.imageDimensions, + image_preview: attachment.imagePreview, + image_url: attachment.imageUrl, + image_type: attachment.imageType, + image_size: attachment.imageSize, + audio_url: attachment.audioUrl, + audio_type: attachment.audioType, + audio_size: attachment.audioSize, + video_url: attachment.videoUrl, + video_type: attachment.videoType, + video_size: attachment.videoSize, + fields: attachment.fields, + button_alignment: attachment.actionButtonsAlignment, + actions: attachment.actions, + type: attachment.type, + description: attachment.description, + }, attachment._unmappedProperties_)); + } + + _convertAttachmentsToApp(attachments) { + if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { + return undefined; + } + + const map = { + collapsed: 'collapsed', + color: 'color', + text: 'text', + timestampLink: 'message_link', + thumbnailUrl: 'thumb_url', + imageDimensions: 'image_dimensions', + imagePreview: 'image_preview', + imageUrl: 'image_url', + imageType: 'image_type', + imageSize: 'image_size', + audioUrl: 'audio_url', + audioType: 'audio_type', + audioSize: 'audio_size', + videoUrl: 'video_url', + videoType: 'video_type', + videoSize: 'video_size', + fields: 'fields', + actionButtonsAlignment: 'button_alignment', + actions: 'actions', + type: 'type', + description: 'description', + author: (attachment) => { + const { + author_name: name, + author_link: link, + author_icon: icon, + } = attachment; + + delete attachment.author_name; + delete attachment.author_link; + delete attachment.author_icon; + + return { name, link, icon }; + }, + title: (attachment) => { + const { + title: value, + title_link: link, + title_link_download: displayDownloadLink, + } = attachment; + + delete attachment.title; + delete attachment.title_link; + delete attachment.title_link_download; + + return { value, link, displayDownloadLink }; + }, + timestamp: (attachment) => { + const result = new Date(attachment.ts); + delete attachment.ts; + return result; + }, + }; + + return attachments.map((attachment) => transformMappedData(attachment, map)); + } +} diff --git a/app/apps/server/converters/rooms.js b/app/apps/server/converters/rooms.js new file mode 100644 index 000000000000..59c16e40ba91 --- /dev/null +++ b/app/apps/server/converters/rooms.js @@ -0,0 +1,95 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; + +import { Rooms, Users } from '../../../models'; + +export class AppRoomsConverter { + constructor(orch) { + this.orch = orch; + } + + convertById(roomId) { + const room = Rooms.findOneById(roomId); + + return this.convertRoom(room); + } + + convertByName(roomName) { + const room = Rooms.findOneByName(roomName); + + return this.convertRoom(room); + } + + convertAppRoom(room) { + if (!room) { + return undefined; + } + + let u; + if (room.creator) { + const creator = Users.findOneById(room.creator.id); + u = { + _id: creator._id, + username: creator.username, + }; + } + + return { + _id: room.id, + fname: room.displayName, + name: room.slugifiedName, + t: room.type, + u, + members: room.members, + default: typeof room.isDefault === 'undefined' ? false : room.isDefault, + ro: typeof room.isReadOnly === 'undefined' ? false : room.isReadOnly, + sysMes: typeof room.displaySystemMessages === 'undefined' ? true : room.displaySystemMessages, + msgs: room.messageCount || 0, + ts: room.createdAt, + _updatedAt: room.updatedAt, + lm: room.lastModifiedAt, + }; + } + + convertRoom(room) { + if (!room) { + return undefined; + } + + let creator; + if (room.u) { + creator = this.orch.getConverters().get('users').convertById(room.u._id); + } + + return { + id: room._id, + displayName: room.fname, + slugifiedName: room.name, + type: this._convertTypeToApp(room.t), + creator, + members: room.members, + isDefault: typeof room.default === 'undefined' ? false : room.default, + isReadOnly: typeof room.ro === 'undefined' ? false : room.ro, + displaySystemMessages: typeof room.sysMes === 'undefined' ? true : room.sysMes, + messageCount: room.msgs, + createdAt: room.ts, + updatedAt: room._updatedAt, + lastModifiedAt: room.lm, + customFields: {}, + }; + } + + _convertTypeToApp(typeChar) { + switch (typeChar) { + case 'c': + return RoomType.CHANNEL; + case 'p': + return RoomType.PRIVATE_GROUP; + case 'd': + return RoomType.DIRECT_MESSAGE; + case 'l': + return RoomType.LIVE_CHAT; + default: + return typeChar; + } + } +} diff --git a/app/apps/server/converters/settings.js b/app/apps/server/converters/settings.js new file mode 100644 index 000000000000..82ffcd2b2f0f --- /dev/null +++ b/app/apps/server/converters/settings.js @@ -0,0 +1,53 @@ +import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; + +import { Settings } from '../../../models'; + +export class AppSettingsConverter { + constructor(orch) { + this.orch = orch; + } + + convertById(settingId) { + const setting = Settings.findOneNotHiddenById(settingId); + + return this.convertToApp(setting); + } + + convertToApp(setting) { + return { + id: setting._id, + type: this._convertTypeToApp(setting.type), + packageValue: setting.packageValue, + values: setting.values, + value: setting.value, + public: setting.public, + hidden: setting.hidden, + group: setting.group, + i18nLabel: setting.i18nLabel, + i18nDescription: setting.i18nDescription, + createdAt: setting.ts, + updatedAt: setting._updatedAt, + }; + } + + _convertTypeToApp(type) { + switch (type) { + case 'boolean': + return SettingType.BOOLEAN; + case 'code': + return SettingType.CODE; + case 'color': + return SettingType.COLOR; + case 'font': + return SettingType.FONT; + case 'int': + return SettingType.NUMBER; + case 'select': + return SettingType.SELECT; + case 'string': + return SettingType.STRING; + default: + return type; + } + } +} diff --git a/app/apps/server/converters/users.js b/app/apps/server/converters/users.js new file mode 100644 index 000000000000..bccbfa0fce53 --- /dev/null +++ b/app/apps/server/converters/users.js @@ -0,0 +1,80 @@ +import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; + +import { Users } from '../../../models'; + +export class AppUsersConverter { + constructor(orch) { + this.orch = orch; + } + + convertById(userId) { + const user = Users.findOneById(userId); + + return this.convertToApp(user); + } + + convertByUsername(username) { + const user = Users.findOneByUsername(username); + + return this.convertToApp(user); + } + + convertToApp(user) { + if (!user) { + return undefined; + } + + const type = this._convertUserTypeToEnum(user.type); + const statusConnection = this._convertStatusConnectionToEnum(user.username, user._id, user.statusConnection); + + return { + id: user._id, + username: user.username, + emails: user.emails, + type, + isEnabled: user.active, + name: user.name, + roles: user.roles, + status: user.status, + statusConnection, + utcOffset: user.utcOffset, + createdAt: user.createdAt, + updatedAt: user._updatedAt, + lastLoginAt: user.lastLogin, + }; + } + + _convertUserTypeToEnum(type) { + switch (type) { + case 'user': + return UserType.USER; + case 'bot': + return UserType.BOT; + case '': + case undefined: + return UserType.UNKNOWN; + default: + console.warn(`A new user type has been added that the Apps don't know about? "${ type }"`); + return type.toUpperCase(); + } + } + + _convertStatusConnectionToEnum(username, userId, status) { + switch (status) { + case 'offline': + return UserStatusConnection.OFFLINE; + case 'online': + return UserStatusConnection.ONLINE; + case 'away': + return UserStatusConnection.AWAY; + case 'busy': + return UserStatusConnection.BUSY; + case undefined: + // This is needed for Livechat guests and Rocket.Cat user. + return UserStatusConnection.UNDEFINED; + default: + console.warn(`The user ${ username } (${ userId }) does not have a valid status (offline, online, away, or busy). It is currently: "${ status }"`); + return !status ? UserStatusConnection.OFFLINE : status.toUpperCase(); + } + } +} diff --git a/app/apps/server/cron.js b/app/apps/server/cron.js new file mode 100644 index 000000000000..0dbed66a9bd3 --- /dev/null +++ b/app/apps/server/cron.js @@ -0,0 +1,116 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; +import { SyncedCron } from 'meteor/littledata:synced-cron'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; + +import { Apps } from './orchestrator'; +import { getWorkspaceAccessToken } from '../../cloud/server'; +import { Settings, Users, Roles } from '../../models/server'; + + +const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdminsAboutInvalidApps(apps) { + const hasInvalidApps = !!apps.find((app) => app.getLatestLicenseValidationResult().hasErrors); + + if (!hasInvalidApps) { + return apps; + } + + const id = 'someAppInInvalidState'; + const title = 'Warning'; + const text = 'There is one or more apps in an invalid state. Click here to review.'; + const rocketCatMessage = 'There is one or more apps in an invalid state. Go to Administration > Apps to review.'; + const link = '/admin/apps'; + + Roles.findUsersInRole('admin').forEach((adminUser) => { + Users.removeBannerById(adminUser._id, { id }); + + try { + Meteor.runAsUser(adminUser._id, () => Meteor.call('createDirectMessage', 'rocket.cat')); + + Meteor.runAsUser('rocket.cat', () => Meteor.call('sendMessage', { + msg: `*${ TAPi18n.__(title, adminUser.language) }*\n${ TAPi18n.__(rocketCatMessage, adminUser.language) }`, + rid: [adminUser._id, 'rocket.cat'].sort().join(''), + })); + } catch (e) { + console.error(e); + } + + Users.addBannerById(adminUser._id, { + id, + priority: 10, + title, + text, + modifiers: ['danger'], + link, + }); + }); + + return apps; +}); + +const notifyAdminsAboutRenewedApps = Meteor.bindEnvironment(function _notifyAdminsAboutRenewedApps(apps) { + const renewedApps = apps.filter((app) => app.getStatus() === AppStatus.DISABLED && app.getPreviousStatus() === AppStatus.INVALID_LICENSE_DISABLED); + + if (renewedApps.length === 0) { + return; + } + + const rocketCatMessage = 'There is one or more disabled apps with valid licenses. Go to Administration > Apps to review.'; + + Roles.findUsersInRole('admin').forEach((adminUser) => { + try { + Meteor.runAsUser(adminUser._id, () => Meteor.call('createDirectMessage', 'rocket.cat')); + + Meteor.runAsUser('rocket.cat', () => Meteor.call('sendMessage', { + msg: `${ TAPi18n.__(rocketCatMessage, adminUser.language) }`, + rid: [adminUser._id, 'rocket.cat'].sort().join(''), + })); + } catch (e) { + console.error(e); + } + }); +}); + +export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(function _appsUpdateMarketplaceInfo() { + const token = getWorkspaceAccessToken(); + const baseUrl = Apps.getMarketplaceUrl(); + const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch(); + + const currentSeats = Users.getActiveLocalUserCount(); + + const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps?seats=${ currentSeats }`; + const options = { + headers: { + Authorization: `Bearer ${ token }`, + }, + }; + + let data = []; + + try { + const result = HTTP.get(fullUrl, options); + + if (Array.isArray(result.data)) { + data = result.data; + } + } catch (err) { + Apps.debugLog(err); + } + + Promise.await( + Apps.updateAppsMarketplaceInfo(data) + .then(notifyAdminsAboutInvalidApps) + .then(notifyAdminsAboutRenewedApps) + ); +}); + +SyncedCron.add({ + name: 'Apps-Engine:check', + schedule: (parser) => parser.text('at 4:00 pm'), + job() { + appsUpdateMarketplaceInfo(); + }, +}); + +SyncedCron.start(); diff --git a/app/apps/server/index.js b/app/apps/server/index.js new file mode 100644 index 000000000000..aa24a2d78926 --- /dev/null +++ b/app/apps/server/index.js @@ -0,0 +1,3 @@ +import './cron'; + +export { Apps } from './orchestrator'; diff --git a/app/apps/server/orchestrator.js b/app/apps/server/orchestrator.js new file mode 100644 index 000000000000..427fae9f5006 --- /dev/null +++ b/app/apps/server/orchestrator.js @@ -0,0 +1,174 @@ +import { Meteor } from 'meteor/meteor'; +import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; + +import { RealAppBridges } from './bridges'; +import { AppMethods, AppsRestApi, AppServerNotifier } from './communication'; +import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter } from './converters'; +import { AppRealStorage, AppRealLogsStorage } from './storage'; +import { settings } from '../../settings'; +import { Permissions, AppsLogsModel, AppsModel, AppsPersistenceModel } from '../../models'; +import { Logger } from '../../logger'; + +export let Apps; + +class AppServerOrchestrator { + constructor() { + this._rocketchatLogger = new Logger('Rocket.Chat Apps'); + Permissions.createOrUpdate('manage-apps', ['admin']); + + this._marketplaceUrl = 'https://marketplace.rocket.chat'; + + this._model = new AppsModel(); + this._logModel = new AppsLogsModel(); + this._persistModel = new AppsPersistenceModel(); + this._storage = new AppRealStorage(this._model); + this._logStorage = new AppRealLogsStorage(this._logModel); + + this._converters = new Map(); + this._converters.set('messages', new AppMessagesConverter(this)); + this._converters.set('rooms', new AppRoomsConverter(this)); + this._converters.set('settings', new AppSettingsConverter(this)); + this._converters.set('users', new AppUsersConverter(this)); + + this._bridges = new RealAppBridges(this); + + this._manager = new AppManager(this._storage, this._logStorage, this._bridges); + + this._communicators = new Map(); + this._communicators.set('methods', new AppMethods(this)); + this._communicators.set('notifier', new AppServerNotifier(this)); + this._communicators.set('restapi', new AppsRestApi(this, this._manager)); + } + + getModel() { + return this._model; + } + + getPersistenceModel() { + return this._persistModel; + } + + getStorage() { + return this._storage; + } + + getLogStorage() { + return this._logStorage; + } + + getConverters() { + return this._converters; + } + + getBridges() { + return this._bridges; + } + + getNotifier() { + return this._communicators.get('notifier'); + } + + getManager() { + return this._manager; + } + + isEnabled() { + return settings.get('Apps_Framework_enabled'); + } + + isLoaded() { + return this.getManager().areAppsLoaded(); + } + + isDebugging() { + return settings.get('Apps_Framework_Development_Mode'); + } + + getRocketChatLogger() { + return this._rocketchatLogger; + } + + debugLog(...args) { + if (this.isDebugging()) { + // eslint-disable-next-line + console.log(...args); + } + } + + getMarketplaceUrl() { + return this._marketplaceUrl; + } + + async load() { + // Don't try to load it again if it has + // already been loaded + if (this.isLoaded()) { + return; + } + + return this._manager.load() + .then((affs) => console.log(`Loaded the Apps Framework and loaded a total of ${ affs.length } Apps!`)) + .catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err)); + } + + async unload() { + // Don't try to unload it if it's already been + // unlaoded or wasn't unloaded to start with + if (!this.isLoaded()) { + return; + } + + return this._manager.unload() + .then(() => console.log('Unloaded the Apps Framework.')) + .catch((err) => console.warn('Failed to unload the Apps Framework!', err)); + } + + async updateAppsMarketplaceInfo(apps = []) { + if (!this.isLoaded()) { + return; + } + + return this._manager.updateAppsMarketplaceInfo(apps) + .then(() => this._manager.get()); + } +} + +settings.addGroup('General', function() { + this.section('Apps', function() { + this.add('Apps_Framework_enabled', true, { + type: 'boolean', + hidden: false, + }); + + this.add('Apps_Framework_Development_Mode', false, { + type: 'boolean', + enableQuery: { + _id: 'Apps_Framework_enabled', + value: true, + }, + public: true, + hidden: false, + }); + }); +}); + +settings.get('Apps_Framework_enabled', (key, isEnabled) => { + // In case this gets called before `Meteor.startup` + if (!Apps) { + return; + } + + if (isEnabled) { + Apps.load(); + } else { + Apps.unload(); + } +}); + +Meteor.startup(function _appServerOrchestrator() { + Apps = new AppServerOrchestrator(); + + if (Apps.isEnabled()) { + Apps.load(); + } +}); diff --git a/app/apps/server/storage/index.js b/app/apps/server/storage/index.js new file mode 100644 index 000000000000..fd1680c1c9c3 --- /dev/null +++ b/app/apps/server/storage/index.js @@ -0,0 +1,4 @@ +import { AppRealLogsStorage } from './logs-storage'; +import { AppRealStorage } from './storage'; + +export { AppRealLogsStorage, AppRealStorage }; diff --git a/packages/rocketchat-apps/server/storage/logs-storage.js b/app/apps/server/storage/logs-storage.js similarity index 100% rename from packages/rocketchat-apps/server/storage/logs-storage.js rename to app/apps/server/storage/logs-storage.js diff --git a/packages/rocketchat-apps/server/storage/storage.js b/app/apps/server/storage/storage.js similarity index 87% rename from packages/rocketchat-apps/server/storage/storage.js rename to app/apps/server/storage/storage.js index 4b8c08eb572d..8fc9c79b705f 100644 --- a/packages/rocketchat-apps/server/storage/storage.js +++ b/app/apps/server/storage/storage.js @@ -44,11 +44,7 @@ export class AppRealStorage extends AppStorage { return reject(e); } - if (doc) { - resolve(doc); - } else { - reject(new Error(`No App found by the id: ${ id }`)); - } + resolve(doc); }); } @@ -74,12 +70,11 @@ export class AppRealStorage extends AppStorage { return new Promise((resolve, reject) => { try { this.db.update({ id: item.id }, item); + resolve(item.id); } catch (e) { return reject(e); } - - this.retrieveOne(item.id).then((updated) => resolve(updated)).catch((err) => reject(err)); - }); + }).then(this.retrieveOne.bind(this)); } remove(id) { diff --git a/app/apps/server/tests/messages.tests.js b/app/apps/server/tests/messages.tests.js new file mode 100644 index 000000000000..9dee0c68bcda --- /dev/null +++ b/app/apps/server/tests/messages.tests.js @@ -0,0 +1,153 @@ +/* eslint-env mocha */ +import 'babel-polyfill'; +import mock from 'mock-require'; +import chai from 'chai'; + +import { AppServerOrchestratorMock } from './mocks/orchestrator.mock'; +import { appMessageMock, appMessageInvalidRoomMock } from './mocks/data/messages.data'; +import { MessagesMock } from './mocks/models/Messages.mock'; +import { RoomsMock } from './mocks/models/Rooms.mock'; +import { UsersMock } from './mocks/models/Users.mock'; + +chai.use(require('chai-datetime')); + +const { expect } = chai; + +mock('../../../models', './mocks/models'); +mock('meteor/random', { + id: () => 1, +}); + +const { AppMessagesConverter } = require('../converters/messages'); + +describe('The AppMessagesConverter instance', function() { + let messagesConverter; + let messagesMock; + + before(function() { + const orchestrator = new AppServerOrchestratorMock(); + + const usersConverter = orchestrator.getConverters().get('users'); + + usersConverter.convertById = function convertUserByIdStub(id) { + return UsersMock.convertedData[id]; + }; + + usersConverter.convertToApp = function convertUserToAppStub(user) { + return { + id: user._id, + username: user.username, + name: user.name, + }; + }; + + orchestrator.getConverters().get('rooms').convertById = function convertRoomByIdStub(id) { + return RoomsMock.convertedData[id]; + }; + + messagesConverter = new AppMessagesConverter(orchestrator); + messagesMock = new MessagesMock(); + }); + + const createdAt = new Date('2019-03-30T01:22:08.389Z'); + const updatedAt = new Date('2019-03-30T01:22:08.412Z'); + + describe('when converting a message from Rocket.Chat to the Engine schema', function() { + it('should return `undefined` when `msgObj` is falsy', function() { + const appMessage = messagesConverter.convertMessage(undefined); + + expect(appMessage).to.be.undefined; + }); + + it('should return a proper schema', function() { + const appMessage = messagesConverter.convertMessage(messagesMock.findOneById('SimpleMessageMock')); + + expect(appMessage).to.have.property('id', 'SimpleMessageMock'); + expect(appMessage).to.have.property('createdAt').which.equalTime(createdAt); + expect(appMessage).to.have.property('updatedAt').which.equalTime(updatedAt); + expect(appMessage).to.have.property('groupable', false); + expect(appMessage).to.have.property('sender').which.includes({ id: 'rocket.cat' }); + expect(appMessage).to.have.property('room').which.includes({ id: 'GENERAL' }); + + expect(appMessage).not.to.have.property('editor'); + expect(appMessage).not.to.have.property('attachments'); + expect(appMessage).not.to.have.property('reactions'); + expect(appMessage).not.to.have.property('avatarUrl'); + expect(appMessage).not.to.have.property('alias'); + expect(appMessage).not.to.have.property('customFields'); + expect(appMessage).not.to.have.property('emoji'); + }); + + it('should not mutate the original message object', function() { + const rocketchatMessageMock = messagesMock.findOneById('SimpleMessageMock'); + + messagesConverter.convertMessage(rocketchatMessageMock); + + expect(rocketchatMessageMock).to.deep.equal({ + _id: 'SimpleMessageMock', + t: 'uj', + rid: 'GENERAL', + ts: new Date('2019-03-30T01:22:08.389Z'), + msg: 'rocket.cat', + u: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + groupable: false, + _updatedAt: new Date('2019-03-30T01:22:08.412Z'), + }); + }); + + it('should add an `_unmappedProperties_` field to the converted message which contains the `t` property of the message', function() { + const appMessage = messagesConverter.convertMessage(messagesMock.findOneById('SimpleMessageMock')); + + expect(appMessage) + .to.have.property('_unmappedProperties_') + .which.has.property('t', 'uj'); + }); + + it('should return basic sender info when it\'s not a Rocket.Chat user (e.g. Livechat Guest)', function() { + const appMessage = messagesConverter.convertMessage(messagesMock.findOneById('LivechatGuestMessageMock')); + + expect(appMessage).to.have.property('sender').which.includes({ + id: 'guest1234', + username: 'guest1234', + name: 'Livechat Guest', + }); + }); + }); + + describe('when converting a message from the Engine schema back to Rocket.Chat', function() { + it('should return `undefined` when `message` is falsy', function() { + const rocketchatMessage = messagesConverter.convertAppMessage(undefined); + + expect(rocketchatMessage).to.be.undefined; + }); + + it('should return a proper schema', function() { + const rocketchatMessage = messagesConverter.convertAppMessage(appMessageMock); + + expect(rocketchatMessage).to.have.property('_id', 'appMessageMock'); + expect(rocketchatMessage).to.have.property('rid', 'GENERAL'); + expect(rocketchatMessage).to.have.property('groupable', false); + expect(rocketchatMessage).to.have.property('ts').which.equalTime(createdAt); + expect(rocketchatMessage).to.have.property('_updatedAt').which.equalTime(updatedAt); + expect(rocketchatMessage).to.have.property('u').which.includes({ + _id: 'rocket.cat', + username: 'rocket.cat', + name: 'Rocket.Cat', + }); + }); + + it('should merge `_unmappedProperties_` into the returned message', function() { + const rocketchatMessage = messagesConverter.convertAppMessage(appMessageMock); + + expect(rocketchatMessage).not.to.have.property('_unmappedProperties_'); + expect(rocketchatMessage).to.have.property('t', 'uj'); + }); + + it('should throw if message has an invalid room', function() { + expect(() => messagesConverter.convertAppMessage(appMessageInvalidRoomMock)).to.throw(Error, 'Invalid room provided on the message.'); + }); + }); +}); diff --git a/app/apps/server/tests/mocks/data/messages.data.js b/app/apps/server/tests/mocks/data/messages.data.js new file mode 100644 index 000000000000..975b294d41d0 --- /dev/null +++ b/app/apps/server/tests/mocks/data/messages.data.js @@ -0,0 +1,115 @@ +export const appMessageMock = { + id: 'appMessageMock', + text: 'rocket.cat', + createdAt: new Date('2019-03-30T01:22:08.389Z'), + updatedAt: new Date('2019-03-30T01:22:08.412Z'), + groupable: false, + room: { + id: 'GENERAL', + displayName: 'general', + slugifiedName: 'general', + type: 'c', + creator: { + username: 'rocket.cat', + emails: [ + { + address: 'rocketcat@rocket.chat', + verified: true, + }, + ], + type: 'bot', + isEnabled: true, + name: 'Rocket.Cat', + roles: [ + 'bot', + ], + status: 'online', + statusConnection: 'online', + utcOffset: 0, + createdAt: new Date('2019-04-13T01:33:14.191Z'), + updatedAt: new Date('2019-04-13T01:33:14.191Z'), + }, + }, + sender: { + id: 'rocket.cat', + username: 'rocket.cat', + emails: [ + { + address: 'rocketcat@rocket.chat', + verified: true, + }, + ], + type: 'bot', + isEnabled: true, + name: 'Rocket.Cat', + roles: [ + 'bot', + ], + status: 'online', + statusConnection: 'online', + utcOffset: 0, + createdAt: new Date('2019-04-13T01:33:14.191Z'), + updatedAt: new Date('2019-04-13T01:33:14.191Z'), + }, + _unmappedProperties_: { + t: 'uj', + }, +}; + +export const appMessageInvalidRoomMock = { + id: 'appMessageInvalidRoomMock', + text: 'rocket.cat', + createdAt: new Date('2019-03-30T01:22:08.389Z'), + updatedAt: new Date('2019-03-30T01:22:08.412Z'), + groupable: false, + room: { + id: 'INVALID IDENTIFICATION', + displayName: 'Mocked Room', + slugifiedName: 'mocked-room', + type: 'c', + creator: { + username: 'rocket.cat', + emails: [ + { + address: 'rocketcat@rocket.chat', + verified: true, + }, + ], + type: 'bot', + isEnabled: true, + name: 'Rocket.Cat', + roles: [ + 'bot', + ], + status: 'online', + statusConnection: 'online', + utcOffset: 0, + createdAt: new Date('2019-04-13T01:33:14.191Z'), + updatedAt: new Date('2019-04-13T01:33:14.191Z'), + }, + }, + sender: { + id: 'rocket.cat', + username: 'rocket.cat', + emails: [ + { + address: 'rocketcat@rocket.chat', + verified: true, + }, + ], + type: 'bot', + isEnabled: true, + name: 'Rocket.Cat', + roles: [ + 'bot', + ], + status: 'online', + statusConnection: 'online', + utcOffset: 0, + createdAt: new Date('2019-04-13T01:33:14.191Z'), + updatedAt: new Date('2019-04-13T01:33:14.191Z'), + }, + _unmappedProperties_: { + t: 'uj', + }, +}; diff --git a/app/apps/server/tests/mocks/models/BaseModel.mock.js b/app/apps/server/tests/mocks/models/BaseModel.mock.js new file mode 100644 index 000000000000..920db0d95fdc --- /dev/null +++ b/app/apps/server/tests/mocks/models/BaseModel.mock.js @@ -0,0 +1,5 @@ +export class BaseModelMock { + findOneById(id) { + return this.data[id]; + } +} diff --git a/app/apps/server/tests/mocks/models/Messages.mock.js b/app/apps/server/tests/mocks/models/Messages.mock.js new file mode 100644 index 000000000000..d21fb5a69ada --- /dev/null +++ b/app/apps/server/tests/mocks/models/Messages.mock.js @@ -0,0 +1,36 @@ +import { BaseModelMock } from './BaseModel.mock'; + +export class MessagesMock extends BaseModelMock { + data = { + SimpleMessageMock: { + _id: 'SimpleMessageMock', + t: 'uj', + rid: 'GENERAL', + ts: new Date('2019-03-30T01:22:08.389Z'), + msg: 'rocket.cat', + u: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + groupable: false, + _updatedAt: new Date('2019-03-30T01:22:08.412Z'), + }, + + LivechatGuestMessageMock: { + _id: 'LivechatGuestMessageMock', + rid: 'LivechatRoom', + msg: 'Help wanted', + token: 'guest-token', + alias: 'Livechat Guest', + ts: new Date('2019-04-06T03:57:28.263Z'), + u: { + _id: 'guest1234', + username: 'guest1234', + name: 'Livechat Guest', + }, + _updatedAt: new Date('2019-04-06T03:57:28.278Z'), + mentions: [], + channels: [], + }, + } +} diff --git a/app/apps/server/tests/mocks/models/Rooms.mock.js b/app/apps/server/tests/mocks/models/Rooms.mock.js new file mode 100644 index 000000000000..a56d7777a28d --- /dev/null +++ b/app/apps/server/tests/mocks/models/Rooms.mock.js @@ -0,0 +1,127 @@ +import { BaseModelMock } from './BaseModel.mock'; + +export class RoomsMock extends BaseModelMock { + data = { + GENERAL: { + _id: 'GENERAL', + ts: new Date('2019-03-27T20:51:36.808Z'), + t: 'c', + name: 'general', + usernames: [], + msgs: 31, + usersCount: 3, + default: true, + _updatedAt: new Date('2019-04-10T17:44:34.931Z'), + lastMessage: { + _id: 1, + t: 'uj', + rid: 'GENERAL', + ts: new Date('2019-03-30T01:22:08.389Z'), + msg: 'rocket.cat', + u: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + groupable: false, + _updatedAt: new Date('2019-03-30T01:22:08.412Z'), + }, + lm: new Date('2019-04-10T17:44:34.873Z'), + }, + + LivechatRoom: { + _id: 'LivechatRoom', + msgs: 41, + usersCount: 1, + lm: new Date('2019-04-07T23:45:25.407Z'), + fname: 'Livechat Guest', + t: 'l', + ts: new Date('2019-04-06T03:56:17.040Z'), + v: { + _id: 'yDLaWs5Rzf5mzQsmB', + username: 'guest-4', + token: 'tkps932ccsl6me7intd3', + status: 'away', + }, + servedBy: { + _id: 'rocket.cat', + username: 'rocket.cat', + ts: new Date('2019-04-06T03:56:17.040Z'), + }, + cl: false, + open: true, + _updatedAt: new Date('2019-04-07T23:45:25.469Z'), + lastMessage: { + _id: 'zgEMhaMLCyDPu7xMn', + rid: 'JceP6CZrpcA4j3NNe', + msg: 'a', + ts: new Date('2019-04-07T23:45:25.407Z'), + u: { + _id: '3Wz2wANqwrd7Hu5Fo', + username: 'dgubert', + name: 'Douglas Gubert', + }, + _updatedAt: new Date('2019-04-07T23:45:25.433Z'), + mentions: [], + channels: [], + }, + metrics: { + v: { + lq: new Date('2019-04-06T03:57:28.263Z'), + }, + reaction: { + fd: new Date('2019-04-06T03:57:17.083Z'), + ft: 60.043, + tt: 52144.278, + }, + response: { + avg: 26072.0655, + fd: new Date('2019-04-06T03:57:17.083Z'), + ft: 59.896, + total: 2, + tt: 52144.131, + }, + servedBy: { + lr: new Date('2019-04-06T18:25:32.394Z'), + }, + }, + responseBy: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + }, + } + + static convertedData = { + GENERAL: { + id: 'GENERAL', + slugifiedName: 'general', + displayName: undefined, + creator: undefined, + createdAt: new Date('2019-03-27T20:51:36.808Z'), + type: 'c', + messageCount: 31, + displaySystemMessages: true, + isReadOnly: false, + isDefault: true, + updatedAt: new Date('2019-04-10T17:44:34.931Z'), + lastModifiedAt: new Date('2019-04-10T17:44:34.873Z'), + customFields: {}, + }, + + LivechatRoom: { + id: 'LivechatRoom', + slugifiedName: undefined, + displayName: 'Livechat Guest', + creator: undefined, + createdAt: new Date('2019-04-06T03:56:17.040Z'), + type: 'l', // Apps-Engine defines the wrong type for livechat rooms + messageCount: 41, + displaySystemMessages: true, + isReadOnly: false, + isDefault: false, + updatedAt: new Date('2019-04-07T23:45:25.469Z'), + lastModifiedAt: new Date('2019-04-07T23:45:25.407Z'), + customFields: {}, + }, + } +} diff --git a/app/apps/server/tests/mocks/models/Users.mock.js b/app/apps/server/tests/mocks/models/Users.mock.js new file mode 100644 index 000000000000..2604a0cb9588 --- /dev/null +++ b/app/apps/server/tests/mocks/models/Users.mock.js @@ -0,0 +1,43 @@ +import { BaseModelMock } from './BaseModel.mock'; + +export class UsersMock extends BaseModelMock { + data = { + 'rocket.cat': { + _id: 'rocket.cat', + createdAt: new Date('2019-03-27T20:51:36.821Z'), + avatarOrigin: 'local', + name: 'Rocket.Cat', + username: 'rocket.cat', + status: 'online', + statusDefault: 'online', + utcOffset: 0, + active: true, + type: 'bot', + _updatedAt: new Date('2019-03-30T01:11:50.496Z'), + roles: [ + 'bot', + ], + }, + } + + static convertedData = { + 'rocket.cat': { + id: 'rocket.cat', + username: 'rocket.cat', + emails: [{ + address: 'rocketcat@rocket.chat', + verified: true, + }], + type: 'bot', + isEnabled: true, + name: 'Rocket.Cat', + roles: ['bot'], + status: 'online', + statusConnection: 'online', + utcOffset: 0, + createdAt: new Date(), + updatedAt: new Date(), + lastLoginAt: undefined, + }, + } +} diff --git a/app/apps/server/tests/mocks/models/index.js b/app/apps/server/tests/mocks/models/index.js new file mode 100644 index 000000000000..725705014607 --- /dev/null +++ b/app/apps/server/tests/mocks/models/index.js @@ -0,0 +1,7 @@ +import { MessagesMock } from './Messages.mock'; +import { RoomsMock } from './Rooms.mock'; +import { UsersMock } from './Users.mock'; + +export const Messages = new MessagesMock(); +export const Rooms = new RoomsMock(); +export const Users = new UsersMock(); diff --git a/app/apps/server/tests/mocks/orchestrator.mock.js b/app/apps/server/tests/mocks/orchestrator.mock.js new file mode 100644 index 000000000000..6429b7a182ef --- /dev/null +++ b/app/apps/server/tests/mocks/orchestrator.mock.js @@ -0,0 +1,105 @@ +export class AppServerOrchestratorMock { + constructor() { + this._marketplaceUrl = 'https://marketplace.rocket.chat'; + + this._model = {}; + this._logModel = {}; + this._persistModel = {}; + this._storage = {}; + this._logStorage = {}; + + this._converters = new Map(); + this._converters.set('messages', {}); + this._converters.set('rooms', {}); + this._converters.set('settings', {}); + this._converters.set('users', {}); + + this._bridges = {}; + + this._manager = {}; + + this._communicators = new Map(); + this._communicators.set('methods', {}); + this._communicators.set('notifier', {}); + this._communicators.set('restapi', {}); + } + + getModel() { + return this._model; + } + + getPersistenceModel() { + return this._persistModel; + } + + getStorage() { + return this._storage; + } + + getLogStorage() { + return this._logStorage; + } + + getConverters() { + return this._converters; + } + + getBridges() { + return this._bridges; + } + + getNotifier() { + return this._communicators.get('notifier'); + } + + getManager() { + return this._manager; + } + + isEnabled() { + return true; + } + + isLoaded() { + return this.getManager().areAppsLoaded(); + } + + isDebugging() { + return true; + } + + debugLog() { + if (this.isDebugging()) { + // eslint-disable-next-line + console.log(...arguments); + } + } + + getMarketplaceUrl() { + return this._marketplaceUrl; + } + + load() { + // Don't try to load it again if it has + // already been loaded + if (this.isLoaded()) { + return; + } + + this._manager.load() + .then((affs) => console.log(`Loaded the Apps Framework and loaded a total of ${ affs.length } Apps!`)) + .catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err)); + } + + unload() { + // Don't try to unload it if it's already been + // unlaoded or wasn't unloaded to start with + if (!this.isLoaded()) { + return; + } + + this._manager.unload() + .then(() => console.log('Unloaded the Apps Framework.')) + .catch((err) => console.warn('Failed to unload the Apps Framework!', err)); + } +} diff --git a/app/assets/index.js b/app/assets/index.js new file mode 100644 index 000000000000..ca39cd0df4b1 --- /dev/null +++ b/app/assets/index.js @@ -0,0 +1 @@ +export * from './server/index'; diff --git a/app/assets/server/assets.js b/app/assets/server/assets.js new file mode 100644 index 000000000000..280dc99c17c5 --- /dev/null +++ b/app/assets/server/assets.js @@ -0,0 +1,533 @@ +import crypto from 'crypto'; + +import { Meteor } from 'meteor/meteor'; +import { WebApp, WebAppInternals } from 'meteor/webapp'; +import { WebAppHashing } from 'meteor/webapp-hashing'; +import _ from 'underscore'; +import sizeOf from 'image-size'; +import sharp from 'sharp'; + +import { settings } from '../../settings'; +import { Settings } from '../../models'; +import { getURL } from '../../utils/lib/getURL'; +import { mime } from '../../utils/lib/mimeTypes'; +import { hasPermission } from '../../authorization'; +import { RocketChatFile } from '../../file'; + + +const RocketChatAssetsInstance = new RocketChatFile.GridFS({ + name: 'assets', +}); + +const assets = { + logo: { + label: 'logo (svg, png, jpg)', + defaultUrl: 'images/logo/logo.svg', + constraints: { + type: 'image', + extensions: ['svg', 'png', 'jpg', 'jpeg'], + width: undefined, + height: undefined, + }, + wizard: { + step: 3, + order: 2, + }, + }, + background: { + label: 'login background (svg, png, jpg)', + defaultUrl: undefined, + constraints: { + type: 'image', + extensions: ['svg', 'png', 'jpg', 'jpeg'], + width: undefined, + height: undefined, + }, + }, + favicon_ico: { + label: 'favicon (ico)', + defaultUrl: 'favicon.ico', + constraints: { + type: 'image', + extensions: ['ico'], + width: undefined, + height: undefined, + }, + }, + favicon: { + label: 'favicon (svg)', + defaultUrl: 'images/logo/icon.svg', + constraints: { + type: 'image', + extensions: ['svg'], + width: undefined, + height: undefined, + }, + }, + favicon_16: { + label: 'favicon 16x16 (png)', + defaultUrl: 'images/logo/favicon-16x16.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 16, + height: 16, + }, + }, + favicon_32: { + label: 'favicon 32x32 (png)', + defaultUrl: 'images/logo/favicon-32x32.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 32, + height: 32, + }, + }, + favicon_192: { + label: 'android-chrome 192x192 (png)', + defaultUrl: 'images/logo/android-chrome-192x192.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 192, + height: 192, + }, + }, + favicon_512: { + label: 'android-chrome 512x512 (png)', + defaultUrl: 'images/logo/android-chrome-512x512.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 512, + height: 512, + }, + }, + touchicon_180: { + label: 'apple-touch-icon 180x180 (png)', + defaultUrl: 'images/logo/apple-touch-icon.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 180, + height: 180, + }, + }, + touchicon_180_pre: { + label: 'apple-touch-icon-precomposed 180x180 (png)', + defaultUrl: 'images/logo/apple-touch-icon-precomposed.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 180, + height: 180, + }, + }, + tile_70: { + label: 'mstile 70x70 (png)', + defaultUrl: 'images/logo/mstile-70x70.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 70, + height: 70, + }, + }, + tile_144: { + label: 'mstile 144x144 (png)', + defaultUrl: 'images/logo/mstile-144x144.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 144, + height: 144, + }, + }, + tile_150: { + label: 'mstile 150x150 (png)', + defaultUrl: 'images/logo/mstile-150x150.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 150, + height: 150, + }, + }, + tile_310_square: { + label: 'mstile 310x310 (png)', + defaultUrl: 'images/logo/mstile-310x310.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 310, + height: 310, + }, + }, + tile_310_wide: { + label: 'mstile 310x150 (png)', + defaultUrl: 'images/logo/mstile-310x150.png', + constraints: { + type: 'image', + extensions: ['png'], + width: 310, + height: 150, + }, + }, + safari_pinned: { + label: 'safari pinned tab (svg)', + defaultUrl: 'images/logo/safari-pinned-tab.svg', + constraints: { + type: 'image', + extensions: ['svg'], + width: undefined, + height: undefined, + }, + }, +}; + +export const RocketChatAssets = new class { + get mime() { + return mime; + } + + get assets() { + return assets; + } + + setAsset(binaryContent, contentType, asset) { + if (!assets[asset]) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { + function: 'RocketChat.Assets.setAsset', + }); + } + + const extension = mime.extension(contentType); + if (assets[asset].constraints.extensions.includes(extension) === false) { + throw new Meteor.Error(contentType, `Invalid file type: ${ contentType }`, { + function: 'RocketChat.Assets.setAsset', + errorTitle: 'error-invalid-file-type', + }); + } + + const file = new Buffer(binaryContent, 'binary'); + if (assets[asset].constraints.width || assets[asset].constraints.height) { + const dimensions = sizeOf(file); + if (assets[asset].constraints.width && assets[asset].constraints.width !== dimensions.width) { + throw new Meteor.Error('error-invalid-file-width', 'Invalid file width', { + function: 'Invalid file width', + }); + } + if (assets[asset].constraints.height && assets[asset].constraints.height !== dimensions.height) { + throw new Meteor.Error('error-invalid-file-height'); + } + } + + const rs = RocketChatFile.bufferToStream(file); + RocketChatAssetsInstance.deleteFile(asset); + + const ws = RocketChatAssetsInstance.createWriteStream(asset, contentType); + ws.on('end', Meteor.bindEnvironment(function() { + return Meteor.setTimeout(function() { + const key = `Assets_${ asset }`; + const value = { + url: `assets/${ asset }.${ extension }`, + defaultUrl: assets[asset].defaultUrl, + }; + + settings.updateById(key, value); + return RocketChatAssets.processAsset(key, value); + }, 200); + })); + + rs.pipe(ws); + } + + unsetAsset(asset) { + if (!assets[asset]) { + throw new Meteor.Error('error-invalid-asset', 'Invalid asset', { + function: 'RocketChat.Assets.unsetAsset', + }); + } + + RocketChatAssetsInstance.deleteFile(asset); + const key = `Assets_${ asset }`; + const value = { + defaultUrl: assets[asset].defaultUrl, + }; + + settings.updateById(key, value); + RocketChatAssets.processAsset(key, value); + } + + refreshClients() { + return process.emit('message', { + refresh: 'client', + }); + } + + processAsset(settingKey, settingValue) { + if (settingKey.indexOf('Assets_') !== 0) { + return; + } + + const assetKey = settingKey.replace(/^Assets_/, ''); + const assetValue = assets[assetKey]; + + if (!assetValue) { + return; + } + + if (!settingValue || !settingValue.url) { + assetValue.cache = undefined; + return; + } + + const file = RocketChatAssetsInstance.getFileSync(assetKey); + if (!file) { + assetValue.cache = undefined; + return; + } + + const hash = crypto.createHash('sha1').update(file.buffer).digest('hex'); + const extension = settingValue.url.split('.').pop(); + + assetValue.cache = { + path: `assets/${ assetKey }.${ extension }`, + cacheable: false, + sourceMapUrl: undefined, + where: 'client', + type: 'asset', + content: file.buffer, + extension, + url: `/assets/${ assetKey }.${ extension }?${ hash }`, + size: file.length, + uploadDate: file.uploadDate, + contentType: file.contentType, + hash, + }; + + return assetValue.cache; + } + + getURL(assetName, options = { cdn: false, full: true }) { + const asset = settings.get(assetName); + const url = asset.url || asset.defaultUrl; + + return getURL(url, options); + } +}(); + +settings.addGroup('Assets'); + +settings.add('Assets_SvgFavicon_Enable', true, { + type: 'boolean', + group: 'Assets', + i18nLabel: 'Enable_Svg_Favicon', +}); + +function addAssetToSetting(asset, value) { + const key = `Assets_${ asset }`; + + settings.add(key, { + defaultUrl: value.defaultUrl, + }, { + type: 'asset', + group: 'Assets', + fileConstraints: value.constraints, + i18nLabel: value.label, + asset, + public: true, + wizard: value.wizard, + }); + + const currentValue = settings.get(key); + + if (typeof currentValue === 'object' && currentValue.defaultUrl !== assets[asset].defaultUrl) { + currentValue.defaultUrl = assets[asset].defaultUrl; + settings.updateById(key, currentValue); + } +} + +for (const key of Object.keys(assets)) { + const value = assets[key]; + addAssetToSetting(key, value); +} + +Settings.find().observe({ + added(record) { + return RocketChatAssets.processAsset(record._id, record.value); + }, + + changed(record) { + return RocketChatAssets.processAsset(record._id, record.value); + }, + + removed(record) { + return RocketChatAssets.processAsset(record._id, undefined); + }, +}); + +Meteor.startup(function() { + return Meteor.setTimeout(function() { + return process.emit('message', { + refresh: 'client', + }); + }, 200); +}); + +const { calculateClientHash } = WebAppHashing; + +WebAppHashing.calculateClientHash = function(manifest, includeFilter, runtimeConfigOverride) { + for (const key of Object.keys(assets)) { + const value = assets[key]; + if (!value.cache && !value.defaultUrl) { + continue; + } + + let cache = {}; + if (value.cache) { + cache = { + path: value.cache.path, + cacheable: value.cache.cacheable, + sourceMapUrl: value.cache.sourceMapUrl, + where: value.cache.where, + type: value.cache.type, + url: value.cache.url, + size: value.cache.size, + hash: value.cache.hash, + }; + } else { + const extension = value.defaultUrl.split('.').pop(); + cache = { + path: `assets/${ key }.${ extension }`, + cacheable: false, + sourceMapUrl: undefined, + where: 'client', + type: 'asset', + url: `/assets/${ key }.${ extension }?v3`, + hash: 'v3', + }; + } + + const manifestItem = _.findWhere(manifest, { + path: key, + }); + + if (manifestItem) { + const index = manifest.indexOf(manifestItem); + manifest[index] = cache; + } else { + manifest.push(cache); + } + } + + return calculateClientHash.call(this, manifest, includeFilter, runtimeConfigOverride); +}; + +Meteor.methods({ + refreshClients() { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'refreshClients', + }); + } + + const _hasPermission = hasPermission(Meteor.userId(), 'manage-assets'); + if (!_hasPermission) { + throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { + method: 'refreshClients', + action: 'Managing_assets', + }); + } + + return RocketChatAssets.refreshClients(); + }, + + unsetAsset(asset) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'unsetAsset', + }); + } + + const _hasPermission = hasPermission(Meteor.userId(), 'manage-assets'); + if (!_hasPermission) { + throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { + method: 'unsetAsset', + action: 'Managing_assets', + }); + } + + return RocketChatAssets.unsetAsset(asset); + }, + + setAsset(binaryContent, contentType, asset) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'setAsset', + }); + } + + const _hasPermission = hasPermission(Meteor.userId(), 'manage-assets'); + if (!_hasPermission) { + throw new Meteor.Error('error-action-not-allowed', 'Managing assets not allowed', { + method: 'setAsset', + action: 'Managing_assets', + }); + } + + RocketChatAssets.setAsset(binaryContent, contentType, asset); + }, +}); + +WebApp.connectHandlers.use('/assets/', Meteor.bindEnvironment(function(req, res, next) { + const params = { + asset: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')).replace(/\.[^.]*$/, ''), + }; + + const file = assets[params.asset] && assets[params.asset].cache; + + const format = req.url.replace(/.*\.([a-z]+)$/, '$1'); + + if (!file) { + const defaultUrl = assets[params.asset] && assets[params.asset].defaultUrl; + if (defaultUrl) { + const assetUrl = format && ['png', 'svg'].includes(format) ? defaultUrl.replace(/(svg|png)$/, format) : defaultUrl; + req.url = `/${ assetUrl }`; + WebAppInternals.staticFilesMiddleware(WebAppInternals.staticFiles, req, res, next); + } else { + res.writeHead(404); + res.end(); + } + + return; + } + + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader) { + if (reqModifiedHeader === (file.uploadDate && file.uploadDate.toUTCString())) { + res.setHeader('Last-Modified', reqModifiedHeader); + res.writeHead(304); + res.end(); + return; + } + } + + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + + if (format && format !== file.extension && ['png', 'jpg', 'jpeg'].includes(format)) { + res.setHeader('Content-Type', `image/${ format }`); + sharp(file.content) + .toFormat(format) + .pipe(res); + return; + } + + res.setHeader('Last-Modified', (file.uploadDate && file.uploadDate.toUTCString()) || new Date().toUTCString()); + res.setHeader('Content-Type', file.contentType); + res.setHeader('Content-Length', file.size); + res.writeHead(200); + res.end(file.content); +})); diff --git a/app/assets/server/index.js b/app/assets/server/index.js new file mode 100644 index 000000000000..8e5b30ff6b6f --- /dev/null +++ b/app/assets/server/index.js @@ -0,0 +1 @@ +export { RocketChatAssets } from './assets'; diff --git a/packages/rocketchat-authorization/README.md b/app/authorization/README.md similarity index 100% rename from packages/rocketchat-authorization/README.md rename to app/authorization/README.md diff --git a/app/authorization/client/hasPermission.js b/app/authorization/client/hasPermission.js new file mode 100644 index 000000000000..994c363428d9 --- /dev/null +++ b/app/authorization/client/hasPermission.js @@ -0,0 +1,65 @@ +import { Meteor } from 'meteor/meteor'; +import { Template } from 'meteor/templating'; + +import { ChatPermissions } from './lib/ChatPermissions'; +import * as Models from '../../models'; + +function atLeastOne(permissions = [], scope, userId) { + userId = userId || Meteor.userId(); + + return permissions.some((permissionId) => { + const permission = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } }); + const roles = (permission && permission.roles) || []; + + return roles.some((roleName) => { + const role = Models.Roles.findOne(roleName, { fields: { scope: 1 } }); + const roleScope = role && role.scope; + const model = Models[roleScope]; + + return model && model.isUserInRole && model.isUserInRole(userId, roleName, scope); + }); + }); +} + +function all(permissions = [], scope, userId) { + userId = userId || Meteor.userId(); + + return permissions.every((permissionId) => { + const permission = ChatPermissions.findOne(permissionId, { fields: { roles: 1 } }); + const roles = (permission && permission.roles) || []; + + return roles.some((roleName) => { + const role = Models.Roles.findOne(roleName, { fields: { scope: 1 } }); + const roleScope = role && role.scope; + const model = Models[roleScope]; + + return model && model.isUserInRole && model.isUserInRole(userId, roleName, scope); + }); + }); +} + +function _hasPermission(permissions, scope, strategy, userId) { + userId = userId || Meteor.userId(); + if (!userId) { + return false; + } + + if (!Models.AuthzCachedCollection.ready.get()) { + return false; + } + + permissions = [].concat(permissions); + return strategy(permissions, scope, userId); +} + +Template.registerHelper('hasPermission', function(permission, scope) { + return _hasPermission(permission, scope, atLeastOne); +}); +Template.registerHelper('userHasAllPermission', function(userId, permission, scope) { + return _hasPermission(permission, scope, all, userId); +}); + +export const hasAllPermission = (permissions, scope) => _hasPermission(permissions, scope, all); +export const hasAtLeastOnePermission = (permissions, scope) => _hasPermission(permissions, scope, atLeastOne); +export const userHasAllPermission = (permissions, scope, userId) => _hasPermission(permissions, scope, all, userId); +export const hasPermission = hasAllPermission; diff --git a/app/authorization/client/hasRole.js b/app/authorization/client/hasRole.js new file mode 100644 index 000000000000..710ae1803cae --- /dev/null +++ b/app/authorization/client/hasRole.js @@ -0,0 +1,6 @@ +import { Roles } from '../../models'; + +export const hasRole = (userId, roleNames, scope) => { + roleNames = [].concat(roleNames); + return Roles.isUserInRoles(userId, roleNames, scope); +}; diff --git a/app/authorization/client/index.js b/app/authorization/client/index.js new file mode 100644 index 000000000000..709df4e2f38c --- /dev/null +++ b/app/authorization/client/index.js @@ -0,0 +1,18 @@ +import { hasAllPermission, hasAtLeastOnePermission, hasPermission, userHasAllPermission } from './hasPermission'; +import { hasRole } from './hasRole'; +import './usersNameChanged'; +import './requiresPermission.html'; +import './route'; +import './startup'; +import './views/permissions.html'; +import './views/permissions'; +import './views/permissionsRole.html'; +import './views/permissionsRole'; + +export { + hasAllPermission, + hasAtLeastOnePermission, + hasRole, + hasPermission, + userHasAllPermission, +}; diff --git a/app/authorization/client/lib/ChatPermissions.js b/app/authorization/client/lib/ChatPermissions.js new file mode 100644 index 000000000000..faafd72fa5e9 --- /dev/null +++ b/app/authorization/client/lib/ChatPermissions.js @@ -0,0 +1,3 @@ +import { AuthzCachedCollection } from '../../../models'; + +export const ChatPermissions = AuthzCachedCollection.collection; diff --git a/packages/rocketchat-authorization/client/requiresPermission.html b/app/authorization/client/requiresPermission.html similarity index 100% rename from packages/rocketchat-authorization/client/requiresPermission.html rename to app/authorization/client/requiresPermission.html diff --git a/app/authorization/client/route.js b/app/authorization/client/route.js new file mode 100644 index 000000000000..5d54d53c8888 --- /dev/null +++ b/app/authorization/client/route.js @@ -0,0 +1,36 @@ +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { BlazeLayout } from 'meteor/kadira:blaze-layout'; + +import { t } from '../../utils/client'; + +FlowRouter.route('/admin/permissions', { + name: 'admin-permissions', + action(/* params*/) { + return BlazeLayout.render('main', { + center: 'permissions', + pageTitle: t('Permissions'), + }); + }, +}); + +FlowRouter.route('/admin/permissions/:name?/edit', { + name: 'admin-permissions-edit', + action(/* params*/) { + return BlazeLayout.render('main', { + center: 'pageContainer', + pageTitle: t('Role_Editing'), + pageTemplate: 'permissionsRole', + }); + }, +}); + +FlowRouter.route('/admin/permissions/new', { + name: 'admin-permissions-new', + action(/* params*/) { + return BlazeLayout.render('main', { + center: 'pageContainer', + pageTitle: t('Role_Editing'), + pageTemplate: 'permissionsRole', + }); + }, +}); diff --git a/app/authorization/client/startup.js b/app/authorization/client/startup.js new file mode 100644 index 000000000000..3656b7d00432 --- /dev/null +++ b/app/authorization/client/startup.js @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasAtLeastOnePermission } from './hasPermission'; +import { CachedCollectionManager } from '../../ui-cached-collection'; +import { AdminBox } from '../../ui-utils/client/lib/AdminBox'; + +Meteor.startup(() => { + CachedCollectionManager.onLogin(() => Meteor.subscribe('roles')); + + AdminBox.addOption({ + href: 'admin-permissions', + i18nLabel: 'Permissions', + icon: 'lock', + permissionGranted() { + return hasAtLeastOnePermission(['access-permissions', 'access-setting-permissions']); + }, + }); +}); diff --git a/app/authorization/client/stylesheets/permissions.css b/app/authorization/client/stylesheets/permissions.css new file mode 100644 index 000000000000..1561d2ebf536 --- /dev/null +++ b/app/authorization/client/stylesheets/permissions.css @@ -0,0 +1,122 @@ +.permissions-manager { + display: flex; + flex-direction: column; + + height: 100%; + + &.page-container { + padding-bottom: 0 !important; + } + + .permission-edit { + display: flex; + flex-direction: column; + + height: 100%; + + padding: 10px; + align-items: center; + } + + .permission-label, + .permission-icon { + display: flex; + + align-items: flex-end; + flex-grow: 1; + } + + .permission-icon { + width: 30px; + + margin-bottom: 0; + flex-grow: 0; + } + + .content { + padding: 0 !important; + } + + .permission-grid { + overflow-x: scroll; + + table-layout: fixed; + + border-collapse: collapse; + + .id-styler { + white-space: nowrap; + + color: #7f7f7f; + + font-size: smaller; + } + + .edit-icon.role-name-edit-icon { + height: 30px; + } + + .role-name { + position: sticky; + + top: 0; + + width: 70px; + + text-align: left; + + vertical-align: middle; + + background: white; + } + + .role-name-edit-icon { + width: 70px; + height: 70px; + + text-align: center; + + vertical-align: middle; + } + + .rotator { + overflow: hidden; + + width: 30px; + height: 130px; + + padding: 10px 0; + + transform: rotate(-180deg); + + white-space: nowrap; + + text-overflow: ellipsis; + writing-mode: vertical-rl; + } + + .admin-table-row { + height: 50px; + } + + td { + overflow: hidden; + } + + .permission-name { + width: 25%; + padding-left: 14px; + + vertical-align: middle; + } + + .permission-checkbox { + text-align: center; + vertical-align: middle; + } + + .icon-edit { + font-size: 1.5em; + } + } +} diff --git a/app/authorization/client/usersNameChanged.js b/app/authorization/client/usersNameChanged.js new file mode 100644 index 000000000000..c498ca472b3c --- /dev/null +++ b/app/authorization/client/usersNameChanged.js @@ -0,0 +1,18 @@ +import { Meteor } from 'meteor/meteor'; + +import { Notifications } from '../../notifications'; +import { RoomRoles } from '../../models'; + +Meteor.startup(function() { + Notifications.onLogged('Users:NameChanged', function({ _id, name }) { + RoomRoles.update({ + 'u._id': _id, + }, { + $set: { + 'u.name': name, + }, + }, { + multi: true, + }); + }); +}); diff --git a/app/authorization/client/views/permissions.html b/app/authorization/client/views/permissions.html new file mode 100644 index 000000000000..0a4af568ada4 --- /dev/null +++ b/app/authorization/client/views/permissions.html @@ -0,0 +1,76 @@ + + \ No newline at end of file diff --git a/app/authorization/client/views/permissions.js b/app/authorization/client/views/permissions.js new file mode 100644 index 000000000000..49c79e4e7e20 --- /dev/null +++ b/app/authorization/client/views/permissions.js @@ -0,0 +1,166 @@ +import { Meteor } from 'meteor/meteor'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; +import { Template } from 'meteor/templating'; + +import { Roles } from '../../../models'; +import { ChatPermissions } from '../lib/ChatPermissions'; +import { hasAllPermission } from '../hasPermission'; + +import { hasAtLeastOnePermission } from '..'; + +import { t } from '../../../utils/client'; +import { SideNav } from '../../../ui-utils/client/lib/SideNav'; + +const whereNotSetting = { + $where: function() { + return this.level !== 'setting'; + }.toString(), +}; + +Template.permissions.helpers({ + roles() { + return Template.instance().roles.get(); + }, + + permissions() { + return ChatPermissions.find(whereNotSetting, // the $where seems to have no effect - filtered as workaround after fetch() + { + sort: { + _id: 1, + }, + }).fetch() + .filter((setting) => !setting.level); + }, + + settingPermissions() { + return ChatPermissions.find({ + level: 'setting', + }, + { + sort: { // sorting seems not to be copied from the publication, we need to request it explicitly in find() + group: 1, + section: 1, + }, + }).fetch() + .filter((setting) => setting.group); // group permissions are assigned implicitly, we can hide them. $exists: {group:false} not supported by Minimongo + }, + + hasPermission() { + return hasAllPermission('access-permissions'); + }, + + hasSettingPermission() { + return hasAllPermission('access-setting-permissions'); + }, + + hasNoPermission() { + return !hasAtLeastOnePermission(['access-permissions', 'access-setting-permissions']); + }, + + settingPermissionsToggled() { + return Template.instance().settingPermissionsToggled.get(); + }, +}); + +Template.permissions.events({ + 'click .js-toggle-setting-permissions'(event, instance) { + instance.settingPermissionsToggled.set(!instance.settingPermissionsToggled.get()); + }, +}); + +Template.permissions.onCreated(function() { + this.settingPermissionsToggled = new ReactiveVar(false); + this.roles = new ReactiveVar([]); + + Tracker.autorun(() => { + this.roles.set(Roles.find().fetch()); + }); +}); + +Template.permissionsTable.helpers({ + granted(roles, role) { + if (roles) { + if (roles.indexOf(role._id) !== -1) { + return 'checked'; + } + } + }, + + permissionName(permission) { + if (permission.level === 'setting') { + let path = ''; + if (permission.group) { + path = `${ t(permission.group) } > `; + } + if (permission.section) { + path = `${ path }${ t(permission.section) } > `; + } + path = `${ path }${ t(permission.settingId) }`; + return path; + } + + return t(permission._id); + }, + + permissionDescription(permission) { + return t(`${ permission._id }_description`); + }, + + hasPermission() { + return hasAllPermission('access-permissions'); + }, +}); + +Template.permissionsTable.events({ + 'click .role-permission'(e, instance) { + const permission = e.currentTarget.getAttribute('data-permission'); + const role = e.currentTarget.getAttribute('data-role'); + + if (!instance.permissionByRole[permission] // the permissino has this role not assigned at all (undefined) + || instance.permissionByRole[permission].indexOf(role) === -1) { + return Meteor.call('authorization:addPermissionToRole', permission, role); + } + return Meteor.call('authorization:removeRoleFromPermission', permission, role); + }, +}); + +Template.permissionsTable.onCreated(function() { + this.roles = new ReactiveVar([]); + this.permissionByRole = {}; + this.actions = { + added: {}, + removed: {}, + }; + + Tracker.autorun(() => { + this.roles.set(Roles.find().fetch()); + + const observer = ChatPermissions.find().observeChanges({ + added: (id, fields) => { + this.permissionByRole[id] = fields.roles; + }, + changed: (id, fields) => { + this.permissionByRole[id] = fields.roles; + }, + removed: (id) => { + delete this.permissionByRole[id]; + }, + }); + + if (this.data.collection === 'Chat') { + ChatPermissions.find(whereNotSetting).observeChanges(observer); + } + + if (this.data.collection === 'Setting') { + ChatPermissions.find({ level: 'setting' }).observeChanges(observer); + } + }); +}); + +Template.permissions.onRendered(() => { + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); diff --git a/app/authorization/client/views/permissionsRole.html b/app/authorization/client/views/permissionsRole.html new file mode 100644 index 000000000000..a8a5ac92c312 --- /dev/null +++ b/app/authorization/client/views/permissionsRole.html @@ -0,0 +1,108 @@ + diff --git a/app/authorization/client/views/permissionsRole.js b/app/authorization/client/views/permissionsRole.js new file mode 100644 index 000000000000..1788e47a2dd2 --- /dev/null +++ b/app/authorization/client/views/permissionsRole.js @@ -0,0 +1,279 @@ +import { Meteor } from 'meteor/meteor'; +import { ReactiveDict } from 'meteor/reactive-dict'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Template } from 'meteor/templating'; +import { Tracker } from 'meteor/tracker'; +import toastr from 'toastr'; + +import { handleError } from '../../../utils/client/lib/handleError'; +import { t } from '../../../utils/lib/tapi18n'; +import { Roles } from '../../../models'; +import { hasAllPermission } from '../hasPermission'; +import { modal } from '../../../ui-utils/client/lib/modal'; +import { SideNav } from '../../../ui-utils/client/lib/SideNav'; +import { APIClient } from '../../../utils/client'; +import { call } from '../../../ui-utils/client'; + +const PAGE_SIZE = 50; + +const loadUsers = async (instance) => { + const offset = instance.state.get('offset'); + + const rid = instance.searchRoom.get(); + + const params = { + role: FlowRouter.getParam('name'), + offset, + count: PAGE_SIZE, + ...rid && { roomId: rid }, + }; + + instance.state.set('loading', true); + const { users } = await APIClient.v1.get('roles.getUsersInRole', params); + + instance.usersInRole.set(instance.usersInRole.curValue.concat(users)); + instance.state.set({ + loading: false, + hasMore: users.length === PAGE_SIZE, + }); +}; + +Template.permissionsRole.helpers({ + role() { + return Roles.findOne({ + _id: FlowRouter.getParam('name'), + }) || {}; + }, + + userInRole() { + return Template.instance().usersInRole.get(); + }, + + editing() { + return FlowRouter.getParam('name') != null; + }, + + emailAddress() { + if (this.emails && this.emails.length > 0) { + return this.emails[0].address; + } + }, + + hasPermission() { + return hasAllPermission('access-permissions'); + }, + + protected() { + return this.protected; + }, + + editable() { + return this._id && !this.protected; + }, + + hasUsers() { + return Template.instance().usersInRole.get().length > 0; + }, + + hasMore() { + return Template.instance().state.get('hasMore'); + }, + + isLoading() { + const instance = Template.instance(); + return (!instance.subscription.ready() || instance.state.get('loading')) && 'btn-loading'; + }, + + searchRoom() { + return Template.instance().searchRoom.get(); + }, + + autocompleteChannelSettings() { + return { + limit: 10, + rules: [ + { + collection: 'CachedChannelList', + subscription: 'channelAndPrivateAutocomplete', + field: 'name', + template: Template.roomSearch, + noMatchTemplate: Template.roomSearchEmpty, + matchAll: true, + sort: 'name', + selector(match) { + return { + name: match, + }; + }, + }, + ], + }; + }, + + autocompleteUsernameSettings() { + const instance = Template.instance(); + return { + limit: 10, + rules: [ + { + collection: 'CachedUserList', + subscription: 'userAutocomplete', + field: 'username', + template: Template.userSearch, + noMatchTemplate: Template.userSearchEmpty, + matchAll: true, + filter: { + exceptions: instance.usersInRole.get(), + }, + selector(match) { + return { + term: match, + }; + }, + sort: 'username', + }, + ], + }; + }, +}); + +Template.permissionsRole.events({ + async 'click .remove-user'(e, instance) { + e.preventDefault(); + modal.open({ + title: t('Are_you_sure'), + text: t('The_user_s_will_be_removed_from_role_s', this.username, FlowRouter.getParam('name')), + type: 'warning', + showCancelButton: true, + confirmButtonColor: '#DD6B55', + confirmButtonText: t('Yes'), + cancelButtonText: t('Cancel'), + closeOnConfirm: false, + html: false, + }, async () => { + await call('authorization:removeUserFromRole', FlowRouter.getParam('name'), this.username, instance.searchRoom.get()); + instance.usersInRole.set(instance.usersInRole.curValue.filter((user) => user.username !== this.username)); + modal.open({ + title: t('Removed'), + text: t('User_removed'), + type: 'success', + timer: 1000, + showConfirmButton: false, + }); + }); + }, + + 'submit #form-role'(e/* , instance*/) { + e.preventDefault(); + const oldBtnValue = e.currentTarget.elements.save.value; + e.currentTarget.elements.save.value = t('Saving'); + const roleData = { + description: e.currentTarget.elements.description.value, + scope: e.currentTarget.elements.scope.value, + mandatory2fa: e.currentTarget.elements.mandatory2fa.checked, + }; + + if (this._id) { + roleData.name = this._id; + } else { + roleData.name = e.currentTarget.elements.name.value; + } + + Meteor.call('authorization:saveRole', roleData, (error/* , result*/) => { + e.currentTarget.elements.save.value = oldBtnValue; + if (error) { + return handleError(error); + } + + toastr.success(t('Saved')); + + if (!this._id) { + return FlowRouter.go('admin-permissions-edit', { + name: roleData.name, + }); + } + }); + }, + + async 'submit #form-users'(e, instance) { + e.preventDefault(); + if (e.currentTarget.elements.username.value.trim() === '') { + return toastr.error(t('Please_fill_a_username')); + } + const oldBtnValue = e.currentTarget.elements.add.value; + e.currentTarget.elements.add.value = t('Saving'); + + try { + await call('authorization:addUserToRole', FlowRouter.getParam('name'), e.currentTarget.elements.username.value, instance.searchRoom.get()); + instance.usersInRole.set([]); + instance.state.set({ + offset: 0, + cache: Date.now(), + }); + toastr.success(t('User_added')); + e.currentTarget.reset(); + } finally { + e.currentTarget.elements.add.value = oldBtnValue; + } + }, + + 'submit #form-search-room'(e) { + return e.preventDefault(); + }, + + 'click .delete-role'(e/* , instance*/) { + e.preventDefault(); + if (this.protected) { + return toastr.error(t('error-delete-protected-role')); + } + + Meteor.call('authorization:deleteRole', this._id, function(error/* , result*/) { + if (error) { + return handleError(error); + } + toastr.success(t('Role_removed')); + FlowRouter.go('admin-permissions'); + }); + }, + + 'click .load-more'(e, t) { + t.state.set('offset', t.state.get('offset') + PAGE_SIZE); + }, + + 'autocompleteselect input[name=room]'(event, template, doc) { + template.searchRoom.set(doc._id); + }, +}); + +Template.permissionsRole.onCreated(async function() { + this.state = new ReactiveDict({ + offset: 0, + loading: false, + hasMore: true, + cache: 0, + }); + this.searchRoom = new ReactiveVar(); + this.searchUsername = new ReactiveVar(); + this.usersInRole = new ReactiveVar([]); + + this.subscription = this.subscribe('roles', FlowRouter.getParam('name')); +}); + +Template.permissionsRole.onRendered(function() { + this.autorun(() => { + this.searchRoom.get(); + this.usersInRole.set([]); + this.state.set({ offset: 0 }); + }); + + this.autorun(() => { + this.state.get('cache'); + loadUsers(this); + }); + + Tracker.afterFlush(() => { + SideNav.setFlex('adminFlex'); + SideNav.openFlex(); + }); +}); diff --git a/app/authorization/index.js b/app/authorization/index.js new file mode 100644 index 000000000000..a67eca871efb --- /dev/null +++ b/app/authorization/index.js @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; + +if (Meteor.isClient) { + module.exports = require('./client/index.js'); +} +if (Meteor.isServer) { + module.exports = require('./server/index.js'); +} diff --git a/app/authorization/server/functions/addUserRoles.js b/app/authorization/server/functions/addUserRoles.js new file mode 100644 index 000000000000..46302e81eb8d --- /dev/null +++ b/app/authorization/server/functions/addUserRoles.js @@ -0,0 +1,32 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { getRoles } from './getRoles'; +import { Users, Roles } from '../../../models'; + +export const addUserRoles = (userId, roleNames, scope) => { + if (!userId || !roleNames) { + return false; + } + + const user = Users.db.findOneById(userId); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + function: 'RocketChat.authz.addUserRoles', + }); + } + + roleNames = [].concat(roleNames); + const existingRoleNames = _.pluck(getRoles(), '_id'); + const invalidRoleNames = _.difference(roleNames, existingRoleNames); + + if (!_.isEmpty(invalidRoleNames)) { + for (const role of invalidRoleNames) { + Roles.createOrUpdate(role); + } + } + + Roles.addUserRoles(userId, roleNames, scope); + + return true; +}; diff --git a/app/authorization/server/functions/canAccessRoom.js b/app/authorization/server/functions/canAccessRoom.js new file mode 100644 index 000000000000..ab4e59b457a7 --- /dev/null +++ b/app/authorization/server/functions/canAccessRoom.js @@ -0,0 +1,40 @@ +import { hasPermissionAsync } from './hasPermission'; +import { Subscriptions } from '../../../models/server/raw'; +import { getValue } from '../../../settings/server/raw'; + +export const roomAccessValidators = [ + async function(room, user = {}) { + if (room && room.t === 'c') { + const anonymous = await getValue('Accounts_AllowAnonymousRead'); + if (!user._id && anonymous === true) { + return true; + } + + return hasPermissionAsync(user._id, 'view-c-room'); + } + }, + async function(room, user) { + if (!room || !user) { + return; + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, user._id); + if (subscription) { + return true; + } + }, +]; + +export const canAccessRoomAsync = async (room, user, extraData) => { + for (let i = 0, total = roomAccessValidators.length; i < total; i++) { + // eslint-disable-next-line no-await-in-loop + const permitted = await roomAccessValidators[i](room, user, extraData); + if (permitted) { + return true; + } + } +}; + +export const canAccessRoom = (room, user, extraData) => Promise.await(canAccessRoomAsync(room, user, extraData)); + +export const addRoomAccessValidator = (validator) => roomAccessValidators.push(validator.bind(this)); diff --git a/app/authorization/server/functions/canSendMessage.js b/app/authorization/server/functions/canSendMessage.js new file mode 100644 index 000000000000..fbae305e07ac --- /dev/null +++ b/app/authorization/server/functions/canSendMessage.js @@ -0,0 +1,38 @@ +import { canAccessRoomAsync } from './canAccessRoom'; +import { hasPermissionAsync } from './hasPermission'; +import { Subscriptions, Rooms } from '../../../models/server/raw'; + +const subscriptionOptions = { + projection: { + blocked: 1, + blocker: 1, + }, +}; + +export const canSendMessageAsync = async (rid, { uid, username }, extraData) => { + const room = await Rooms.findOneById(rid); + + if (!await canAccessRoomAsync(room, { _id: uid, username }, extraData)) { + throw new Error('error-not-allowed'); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, uid, subscriptionOptions); + if (subscription.blocked || subscription.blocker) { + throw new Error('room_is_blocked'); + } + + 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 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/authorization/server/functions/getRoles.js b/app/authorization/server/functions/getRoles.js new file mode 100644 index 000000000000..9d20c72d29a9 --- /dev/null +++ b/app/authorization/server/functions/getRoles.js @@ -0,0 +1,3 @@ +import { Roles } from '../../../models'; + +export const getRoles = () => Roles.find().fetch(); diff --git a/app/authorization/server/functions/getUsersInRole.js b/app/authorization/server/functions/getUsersInRole.js new file mode 100644 index 000000000000..27c369acf9ff --- /dev/null +++ b/app/authorization/server/functions/getUsersInRole.js @@ -0,0 +1,3 @@ +import { Roles } from '../../../models'; + +export const getUsersInRole = (roleName, scope, options) => Roles.findUsersInRole(roleName, scope, options); diff --git a/app/authorization/server/functions/hasPermission.js b/app/authorization/server/functions/hasPermission.js new file mode 100644 index 000000000000..fffcd9fb886e --- /dev/null +++ b/app/authorization/server/functions/hasPermission.js @@ -0,0 +1,48 @@ +import { Permissions, Roles } from '../../../models/server/raw'; + +async function atLeastOne(userId, permissions = [], scope) { + for (let i = 0, total = permissions.length; i < total; i++) { + const permissionId = permissions[i]; + + // eslint-disable-next-line no-await-in-loop + const permission = await Permissions.findOne({ _id: permissionId }); + // eslint-disable-next-line no-await-in-loop + const found = await Roles.isUserInRoles(userId, permission.roles, scope); + if (found) { + return true; + } + } + + return false; +} + +async function all(userId, permissions = [], scope) { + for (let i = 0, total = permissions.length; i < total; i++) { + const permissionId = permissions[i]; + + // eslint-disable-next-line no-await-in-loop + const permission = await Permissions.findOne({ _id: permissionId }); + // eslint-disable-next-line no-await-in-loop + const found = await Roles.isUserInRoles(userId, permission.roles, scope); + if (!found) { + return false; + } + } + + return true; +} + +function _hasPermission(userId, permissions, scope, strategy) { + if (!userId) { + return false; + } + return strategy(userId, [].concat(permissions), scope); +} + +export const hasAllPermissionAsync = async (userId, permissions, scope) => _hasPermission(userId, permissions, scope, all); +export const hasPermissionAsync = async (userId, permissionId, scope) => _hasPermission(userId, permissionId, scope, all); +export const hasAtLeastOnePermissionAsync = async (userId, permissions, scope) => _hasPermission(userId, permissions, scope, atLeastOne); + +export const hasAllPermission = (userId, permissions, scope) => Promise.await(hasAllPermissionAsync(userId, permissions, scope)); +export const hasPermission = (userId, permissionId, scope) => Promise.await(hasPermissionAsync(userId, permissionId, scope)); +export const hasAtLeastOnePermission = (userId, permissions, scope) => Promise.await(hasAtLeastOnePermissionAsync(userId, permissions, scope)); diff --git a/app/authorization/server/functions/hasRole.js b/app/authorization/server/functions/hasRole.js new file mode 100644 index 000000000000..879aeaa01241 --- /dev/null +++ b/app/authorization/server/functions/hasRole.js @@ -0,0 +1,6 @@ +import { Roles } from '../../../models'; + +export const hasRole = (userId, roleNames, scope) => { + roleNames = [].concat(roleNames); + return Roles.isUserInRoles(userId, roleNames, scope); +}; diff --git a/app/authorization/server/functions/removeUserFromRoles.js b/app/authorization/server/functions/removeUserFromRoles.js new file mode 100644 index 000000000000..b08c2778addb --- /dev/null +++ b/app/authorization/server/functions/removeUserFromRoles.js @@ -0,0 +1,33 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { getRoles } from './getRoles'; +import { Users, Roles } from '../../../models'; + +export const removeUserFromRoles = (userId, roleNames, scope) => { + if (!userId || !roleNames) { + return false; + } + + const user = Users.findOneById(userId); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + function: 'RocketChat.authz.removeUserFromRoles', + }); + } + + roleNames = [].concat(roleNames); + const existingRoleNames = _.pluck(getRoles(), '_id'); + const invalidRoleNames = _.difference(roleNames, existingRoleNames); + + if (!_.isEmpty(invalidRoleNames)) { + throw new Meteor.Error('error-invalid-role', 'Invalid role', { + function: 'RocketChat.authz.removeUserFromRoles', + }); + } + + Roles.removeUserRoles(userId, roleNames, scope); + + return true; +}; diff --git a/app/authorization/server/index.js b/app/authorization/server/index.js new file mode 100644 index 000000000000..37a853a1f101 --- /dev/null +++ b/app/authorization/server/index.js @@ -0,0 +1,41 @@ +import { addUserRoles } from './functions/addUserRoles'; +import { + addRoomAccessValidator, + canAccessRoom, + roomAccessValidators, +} from './functions/canAccessRoom'; +import { canSendMessage } from './functions/canSendMessage'; +import { getRoles } from './functions/getRoles'; +import { getUsersInRole } from './functions/getUsersInRole'; +import { + hasAllPermission, + hasAtLeastOnePermission, + hasPermission, +} from './functions/hasPermission'; +import { hasRole } from './functions/hasRole'; +import { removeUserFromRoles } from './functions/removeUserFromRoles'; +import './methods/addPermissionToRole'; +import './methods/addUserToRole'; +import './methods/deleteRole'; +import './methods/removeRoleFromPermission'; +import './methods/removeUserFromRole'; +import './methods/saveRole'; +import './publications/permissions'; +import './publications/roles'; +import './publications/usersInRole'; +import './startup'; + +export { + getRoles, + getUsersInRole, + hasRole, + removeUserFromRoles, + canSendMessage, + addRoomAccessValidator, + roomAccessValidators, + addUserRoles, + canAccessRoom, + hasAllPermission, + hasAtLeastOnePermission, + hasPermission, +}; diff --git a/app/authorization/server/methods/addPermissionToRole.js b/app/authorization/server/methods/addPermissionToRole.js new file mode 100644 index 000000000000..4c639243046c --- /dev/null +++ b/app/authorization/server/methods/addPermissionToRole.js @@ -0,0 +1,31 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../../models'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 'authorization:addPermissionToRole'(permission, role) { + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions') + || (permission.level === 'setting' && !hasPermission(Meteor.userId(), 'access-setting-permissions')) + ) { + throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', { + method: 'authorization:addPermissionToRole', + action: 'Adding_permission', + }); + } + + // for setting-based-permissions, authorize the group access as well + const addParentPermissions = function(permissionId, role) { + const permission = Permissions.findOneById(permissionId); + if (permission.groupPermissionId) { + const groupPermission = Permissions.findOneById(permission.groupPermissionId); + if (groupPermission.roles.indexOf(role) === -1) { + Permissions.addRole(permission.groupPermissionId, role); + } + } + }; + + addParentPermissions(permission, role); + return Permissions.addRole(permission, role); + }, +}); diff --git a/app/authorization/server/methods/addUserToRole.js b/app/authorization/server/methods/addUserToRole.js new file mode 100644 index 000000000000..b9d8302bc0b3 --- /dev/null +++ b/app/authorization/server/methods/addUserToRole.js @@ -0,0 +1,66 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { Users, Roles } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Notifications } from '../../../notifications/server'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 'authorization:addUserToRole'(roleName, username, scope) { + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { + method: 'authorization:addUserToRole', + action: 'Accessing_permissions', + }); + } + + if (!roleName || !_.isString(roleName) || !username || !_.isString(username)) { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { + method: 'authorization:addUserToRole', + }); + } + + if (roleName === 'admin' && !hasPermission(Meteor.userId(), 'assign-admin-role')) { + throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', { + method: 'authorization:addUserToRole', + action: 'Assign_admin', + }); + } + + const user = Users.findOneByUsernameIgnoringCase(username, { + fields: { + _id: 1, + }, + }); + + if (!user || !user._id) { + throw new Meteor.Error('error-user-not-found', 'User not found', { + method: 'authorization:addUserToRole', + }); + } + + // verify if user can be added to given scope + if (scope && !Roles.canAddUserToRole(user._id, roleName, scope)) { + throw new Meteor.Error('error-invalid-user', 'User is not part of given room', { + method: 'authorization:addUserToRole', + }); + } + + const add = Roles.addUserRoles(user._id, roleName, scope); + + if (settings.get('UI_DisplayRoles')) { + Notifications.notifyLogged('roles-change', { + type: 'added', + _id: roleName, + u: { + _id: user._id, + username, + }, + scope, + }); + } + + return add; + }, +}); diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.js new file mode 100644 index 000000000000..8613e1761b0a --- /dev/null +++ b/app/authorization/server/methods/deleteRole.js @@ -0,0 +1,40 @@ +import { Meteor } from 'meteor/meteor'; + +import * as Models from '../../../models/server'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 'authorization:deleteRole'(roleName) { + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { + method: 'authorization:deleteRole', + action: 'Accessing_permissions', + }); + } + + const role = Models.Roles.findOne(roleName); + if (!role) { + throw new Meteor.Error('error-invalid-role', 'Invalid role', { + method: 'authorization:deleteRole', + }); + } + + if (role.protected) { + throw new Meteor.Error('error-delete-protected-role', 'Cannot delete a protected role', { + method: 'authorization:deleteRole', + }); + } + + const roleScope = role.scope || 'Users'; + const model = Models[roleScope]; + const existingUsers = model && model.findUsersInRoles && model.findUsersInRoles(roleName); + + if (existingUsers && existingUsers.count() > 0) { + throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use', { + method: 'authorization:deleteRole', + }); + } + + return Models.Roles.remove(role.name); + }, +}); diff --git a/app/authorization/server/methods/removeRoleFromPermission.js b/app/authorization/server/methods/removeRoleFromPermission.js new file mode 100644 index 000000000000..b1937430baea --- /dev/null +++ b/app/authorization/server/methods/removeRoleFromPermission.js @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../../models/server'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 'authorization:removeRoleFromPermission'(permission, role) { + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions') + || (permission.level === 'setting' && !hasPermission(Meteor.userId(), 'access-setting-permissions')) + ) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { + method: 'authorization:removeRoleFromPermission', + action: 'Accessing_permissions', + }); + } + + // for setting based permissions, revoke the group permission once all setting permissions + // related to this group have been removed + const removeStaleParentPermissions = function(permissionId, role) { + const permission = Permissions.findOneById(permissionId); + if (permission.groupPermissionId) { + const groupPermission = Permissions.findOneById(permission.groupPermissionId); + if (groupPermission.roles.indexOf(role) !== -1) { + // the role has the group permission assigned, so check whether it's still needed + if (Permissions.find({ + groupPermissionId: permission.groupPermissionId, + roles: role, + }).count() === 0) { + Permissions.removeRole(permission.groupPermissionId, role); + } + } + } + }; + Permissions.removeRole(permission, role); + removeStaleParentPermissions(permission, role); + }, +}); diff --git a/app/authorization/server/methods/removeUserFromRole.js b/app/authorization/server/methods/removeUserFromRole.js new file mode 100644 index 000000000000..4ccec8b2a7c6 --- /dev/null +++ b/app/authorization/server/methods/removeUserFromRole.js @@ -0,0 +1,71 @@ +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { Roles } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Notifications } from '../../../notifications/server'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 'authorization:removeUserFromRole'(roleName, username, scope) { + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', { + method: 'authorization:removeUserFromRole', + action: 'Accessing_permissions', + }); + } + + if (!roleName || !_.isString(roleName) || !username || !_.isString(username)) { + throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { + method: 'authorization:removeUserFromRole', + }); + } + + const user = Meteor.users.findOne({ + username, + }, { + fields: { + _id: 1, + roles: 1, + }, + }); + + if (!user || !user._id) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'authorization:removeUserFromRole', + }); + } + + // prevent removing last user from admin role + if (roleName === 'admin') { + const adminCount = Meteor.users.find({ + roles: { + $in: ['admin'], + }, + }).count(); + + const userIsAdmin = user.roles.indexOf('admin') > -1; + if (adminCount === 1 && userIsAdmin) { + throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', { + method: 'removeUserFromRole', + action: 'Remove_last_admin', + }); + } + } + + const remove = Roles.removeUserRoles(user._id, roleName, scope); + if (settings.get('UI_DisplayRoles')) { + Notifications.notifyLogged('roles-change', { + type: 'removed', + _id: roleName, + u: { + _id: user._id, + username, + }, + scope, + }); + } + + return remove; + }, +}); diff --git a/app/authorization/server/methods/saveRole.js b/app/authorization/server/methods/saveRole.js new file mode 100644 index 000000000000..5f0998e7d493 --- /dev/null +++ b/app/authorization/server/methods/saveRole.js @@ -0,0 +1,37 @@ +import { Meteor } from 'meteor/meteor'; + +import { Roles } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Notifications } from '../../../notifications/server'; +import { hasPermission } from '../functions/hasPermission'; + +Meteor.methods({ + 'authorization:saveRole'(roleData) { + if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { + throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { + method: 'authorization:saveRole', + action: 'Accessing_permissions', + }); + } + + if (!roleData.name) { + throw new Meteor.Error('error-role-name-required', 'Role name is required', { + method: 'authorization:saveRole', + }); + } + + if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { + roleData.scope = 'Users'; + } + + const update = Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); + if (settings.get('UI_DisplayRoles')) { + Notifications.notifyLogged('roles-change', { + type: 'changed', + _id: roleData.name, + }); + } + + return update; + }, +}); diff --git a/app/authorization/server/publications/permissions.js b/app/authorization/server/publications/permissions.js new file mode 100644 index 000000000000..6e53f2752cd1 --- /dev/null +++ b/app/authorization/server/publications/permissions.js @@ -0,0 +1,46 @@ +import { Meteor } from 'meteor/meteor'; + +import Permissions from '../../../models/server/models/Permissions'; +import Settings from '../../../models/server/models/Settings'; +import { Notifications } from '../../../notifications'; + +Meteor.methods({ + 'permissions/get'(updatedAt) { + // TODO: should we return this for non logged users? + // TODO: we could cache this collection + + const records = Permissions.find().fetch(); + + if (updatedAt instanceof Date) { + return { + update: records.filter((record) => record._updatedAt > updatedAt), + remove: Permissions.trashFindDeletedAfter(updatedAt, {}, { fields: { _id: 1, _deletedAt: 1 } }).fetch(), + }; + } + + 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; + } + + if (data.level && data.level === 'setting') { + // if the permission changes, the effect on the visible settings depends on the role affected. + // The selected-settings-based consumers have to react accordingly and either add or remove the + // setting from the user's collection + const setting = Settings.findOneById(data.settingId); + Notifications.notifyLoggedInThisInstance('private-settings-changed', 'auth', setting); + } + + Notifications.notifyLoggedInThisInstance('permissions-changed', clientAction, data); +}); 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/index.js b/app/authorization/server/publications/permissions/index.js new file mode 100644 index 000000000000..45f4f9d01c1a --- /dev/null +++ b/app/authorization/server/publications/permissions/index.js @@ -0,0 +1,22 @@ +import { Meteor } from 'meteor/meteor'; + +import Permissions from '../../../../models/server/models/Permissions'; +import './emitter'; + +Meteor.methods({ + 'permissions/get'(updatedAt) { + // TODO: should we return this for non logged users? + // TODO: we could cache this collection + + const records = Permissions.find().fetch(); + + if (updatedAt instanceof Date) { + return { + update: records.filter((record) => record._updatedAt > updatedAt), + remove: Permissions.trashFindDeletedAfter(updatedAt, {}, { fields: { _id: 1, _deletedAt: 1 } }).fetch(), + }; + } + + return records; + }, +}); diff --git a/app/authorization/server/publications/roles.js b/app/authorization/server/publications/roles.js new file mode 100644 index 000000000000..379bca0fbc95 --- /dev/null +++ b/app/authorization/server/publications/roles.js @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; + +import { Roles } from '../../../models'; + +Meteor.publish('roles', function() { + if (!this.userId) { + return this.ready(); + } + + return Roles.find(); +}); diff --git a/app/authorization/server/publications/usersInRole.js b/app/authorization/server/publications/usersInRole.js new file mode 100644 index 000000000000..f7eb22146e45 --- /dev/null +++ b/app/authorization/server/publications/usersInRole.js @@ -0,0 +1,27 @@ +import { Meteor } from 'meteor/meteor'; + +import { hasPermission } from '../functions/hasPermission'; +import { getUsersInRole } from '../functions/getUsersInRole'; + +Meteor.publish('usersInRole', function(roleName, scope, limit = 50) { + console.warn('The publication "usersInRole" is deprecated and will be removed after version v2.0.0'); + + if (!this.userId) { + return this.ready(); + } + + if (!hasPermission(this.userId, 'access-permissions')) { + return this.error(new Meteor.Error('error-not-allowed', 'Not allowed', { + publish: 'usersInRole', + })); + } + + const options = { + limit, + sort: { + name: 1, + }, + }; + + return getUsersInRole(roleName, scope, options); +}); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js new file mode 100644 index 000000000000..228c52f69636 --- /dev/null +++ b/app/authorization/server/startup.js @@ -0,0 +1,186 @@ +/* eslint no-multi-spaces: 0 */ +import { Meteor } from 'meteor/meteor'; + +import { Roles, Permissions, Settings } from '../../models'; +import { settings } from '../../settings/server'; + +Meteor.startup(function() { + // Note: + // 1.if we need to create a role that can only edit channel message, but not edit group message + // then we can define edit--message instead of edit-message + // 2. admin, moderator, and user roles should not be deleted as they are referened in the code. + const permissions = [ + { _id: 'access-permissions', roles: ['admin'] }, + { _id: 'access-setting-permissions', roles: ['admin'] }, + { _id: 'add-oauth-service', roles: ['admin'] }, + { _id: 'add-user-to-joined-room', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'add-user-to-any-c-room', roles: ['admin'] }, + { _id: 'add-user-to-any-p-room', roles: [] }, + { _id: 'api-bypass-rate-limit', roles: ['admin', 'bot'] }, + { _id: 'archive-room', roles: ['admin', 'owner'] }, + { _id: 'assign-admin-role', roles: ['admin'] }, + { _id: 'assign-roles', roles: ['admin'] }, + { _id: 'ban-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'bulk-create-c', roles: ['admin'] }, + { _id: 'bulk-register-user', roles: ['admin'] }, + { _id: 'create-c', roles: ['admin', 'user', 'bot'] }, + { _id: 'create-d', roles: ['admin', 'user', 'bot'] }, + { _id: 'create-p', roles: ['admin', 'user', 'bot'] }, + { _id: 'create-personal-access-tokens', roles: ['admin', 'user'] }, + { _id: 'create-user', roles: ['admin'] }, + { _id: 'clean-channel-history', roles: ['admin'] }, + { _id: 'delete-c', roles: ['admin', 'owner'] }, + { _id: 'delete-d', roles: ['admin'] }, + { _id: 'delete-message', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'delete-p', roles: ['admin', 'owner'] }, + { _id: 'delete-user', roles: ['admin'] }, + { _id: 'edit-message', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-other-user-active-status', roles: ['admin'] }, + { _id: 'edit-other-user-info', roles: ['admin'] }, + { _id: 'edit-other-user-password', roles: ['admin'] }, + { _id: 'edit-other-user-avatar', roles: ['admin'] }, + { _id: 'edit-privileged-setting', roles: ['admin'] }, + { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'edit-room-retention-policy', roles: ['admin'] }, + { _id: 'force-delete-message', roles: ['admin', 'owner'] }, + { _id: 'join-without-join-code', roles: ['admin', 'bot'] }, + { _id: 'leave-c', roles: ['admin', 'user', 'bot', 'anonymous'] }, + { _id: 'leave-p', roles: ['admin', 'user', 'bot', 'anonymous'] }, + { _id: 'manage-assets', roles: ['admin'] }, + { _id: 'manage-emoji', roles: ['admin'] }, + { _id: 'manage-user-status', roles: ['admin'] }, + { _id: 'manage-integrations', roles: ['admin'] }, + { _id: 'manage-own-integrations', roles: ['admin'] }, + { _id: 'manage-oauth-apps', roles: ['admin'] }, + { _id: 'manage-selected-settings', roles: ['admin'] }, + { _id: 'mention-all', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'mention-here', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'mute-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'remove-user', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'reset-other-user-e2e-key', roles: ['admin'] }, + { _id: 'run-import', roles: ['admin'] }, + { _id: 'run-migration', roles: ['admin'] }, + { _id: 'set-moderator', roles: ['admin', 'owner'] }, + { _id: 'set-owner', roles: ['admin', 'owner'] }, + { _id: 'send-many-messages', roles: ['admin', 'bot'] }, + { _id: 'set-leader', roles: ['admin', 'owner'] }, + { _id: 'unarchive-room', roles: ['admin'] }, + { _id: 'view-c-room', roles: ['admin', 'user', 'bot', 'anonymous'] }, + { _id: 'user-generate-access-token', roles: ['admin'] }, + { _id: 'view-d-room', roles: ['admin', 'user', 'bot'] }, + { _id: 'view-full-other-user-info', roles: ['admin'] }, + { _id: 'view-history', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-joined-room', roles: ['guest', 'bot', 'anonymous'] }, + { _id: 'view-join-code', roles: ['admin'] }, + { _id: 'view-logs', roles: ['admin'] }, + { _id: 'view-other-user-channels', roles: ['admin'] }, + { _id: 'view-p-room', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-privileged-setting', roles: ['admin'] }, + { _id: 'view-room-administration', roles: ['admin'] }, + { _id: 'view-statistics', roles: ['admin'] }, + { _id: 'view-user-administration', roles: ['admin'] }, + { _id: 'preview-c-room', roles: ['admin', 'user', 'anonymous'] }, + { _id: 'view-outside-room', roles: ['admin', 'owner', 'moderator', 'user'] }, + { _id: 'view-broadcast-member-list', roles: ['admin', 'owner', 'moderator'] }, + { _id: 'call-management', roles: ['admin', 'owner', 'moderator'] }, + ]; + + for (const permission of permissions) { + if (!Permissions.findOneById(permission._id)) { + Permissions.upsert(permission._id, { $set: permission }); + } + } + + const defaultRoles = [ + { name: 'admin', scope: 'Users', description: 'Admin' }, + { name: 'moderator', scope: 'Subscriptions', description: 'Moderator' }, + { name: 'leader', scope: 'Subscriptions', description: 'Leader' }, + { name: 'owner', scope: 'Subscriptions', description: 'Owner' }, + { name: 'user', scope: 'Users', description: '' }, + { name: 'bot', scope: 'Users', description: '' }, + { name: 'guest', scope: 'Users', description: '' }, + { name: 'anonymous', scope: 'Users', description: '' }, + ]; + + for (const role of defaultRoles) { + Roles.upsert({ _id: role.name }, { $setOnInsert: { scope: role.scope, description: role.description || '', protected: true, mandatory2fa: false } }); + } + + + // setting-based permissions + const getSettingPermissionId = function(settingId) { + return `change-setting-${ settingId }`; + }; + + const getPreviousPermissions = function(settingId) { + const previousSettingPermissions = {}; + + const selector = { level: 'setting' }; + if (settingId) { + selector.settingId = settingId; + } + + Permissions.find(selector).fetch().forEach( + function(permission) { + previousSettingPermissions[permission._id] = permission; + }); + return previousSettingPermissions; + }; + const createSettingPermission = function(setting, previousSettingPermissions) { + const permissionId = getSettingPermissionId(setting._id); + const permission = { + _id: permissionId, + level: 'setting', + // copy those setting-properties which are needed to properly publish the setting-based permissions + settingId: setting._id, + group: setting.group, + section: setting.section, + sorter: setting.sorter, + }; + // copy previously assigned roles if available + if (previousSettingPermissions[permissionId] && previousSettingPermissions[permissionId].roles) { + permission.roles = previousSettingPermissions[permissionId].roles; + } else { + permission.roles = []; + } + if (setting.group) { + permission.groupPermissionId = getSettingPermissionId(setting.group); + } + if (setting.section) { + permission.sectionPermissionId = getSettingPermissionId(setting.section); + } + Permissions.upsert(permission._id, { $set: permission }); + delete previousSettingPermissions[permissionId]; + }; + + const createPermissionsForExistingSettings = function() { + const previousSettingPermissions = getPreviousPermissions(); + + Settings.findNotHidden().fetch().forEach((setting) => { + createSettingPermission(setting, previousSettingPermissions); + }); + + // remove permissions for non-existent settings + for (const obsoletePermission in previousSettingPermissions) { + if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { + Permissions.remove({ _id: obsoletePermission }); + } + } + }; + + // for each setting which already exists, create a permission to allow changing just this one setting + createPermissionsForExistingSettings(); + + // register a callback for settings for be create in higher-level-packages + const createPermissionForAddedSetting = function(settingId) { + const previousSettingPermissions = getPreviousPermissions(settingId); + const setting = Settings.findOneById(settingId); + if (setting) { + if (!setting.hidden) { + createSettingPermission(setting, previousSettingPermissions); + } + } + }; + + settings.onload('*', createPermissionForAddedSetting); +}); diff --git a/app/autolinker/client/client.js b/app/autolinker/client/client.js new file mode 100644 index 000000000000..b7e7f905bbce --- /dev/null +++ b/app/autolinker/client/client.js @@ -0,0 +1,75 @@ +import { Meteor } from 'meteor/meteor'; +import s from 'underscore.string'; +import Autolinker from 'autolinker'; + +import { settings } from '../../settings'; +import { callbacks } from '../../callbacks'; + +const createAutolinker = () => { + const regUrls = new RegExp(settings.get('AutoLinker_UrlsRegExp')); + + const replaceAutolinkerMatch = (match) => { + if (match.getType() !== 'url') { + return null; + } + + if (!regUrls.test(match.matchedText)) { + return null; + } + + if (match.matchedText.indexOf(Meteor.absoluteUrl()) === 0) { + const tag = match.buildTag(); + tag.setAttr('target', ''); + return tag; + } + + return true; + }; + + return new Autolinker({ + stripPrefix: settings.get('AutoLinker_StripPrefix'), + urls: { + schemeMatches: settings.get('AutoLinker_Urls_Scheme'), + wwwMatches: settings.get('AutoLinker_Urls_www'), + tldMatches: settings.get('AutoLinker_Urls_TLD'), + }, + email: settings.get('AutoLinker_Email'), + phone: settings.get('AutoLinker_Phone'), + twitter: false, + stripTrailingSlash: false, + replaceFn: replaceAutolinkerMatch, + }); +}; + +const renderMessage = (message) => { + if (settings.get('AutoLinker') !== true) { + return message; + } + + if (!s.trim(message.html)) { + return message; + } + + let msgParts; + let regexTokens; + if (message.tokens && message.tokens.length) { + regexTokens = new RegExp(`(${ (message.tokens || []).map(({ token }) => RegExp.escape(token)) })`, 'g'); + msgParts = message.html.split(regexTokens); + } else { + msgParts = [message.html]; + } + const autolinker = createAutolinker(); + message.html = msgParts + .map((msgPart) => { + if (regexTokens && regexTokens.test(msgPart)) { + return msgPart; + } + + return autolinker.link(msgPart); + }) + .join(''); + + return message; +}; + +callbacks.add('renderMessage', renderMessage, callbacks.priority.LOW, 'autolinker'); diff --git a/app/autolinker/client/index.js b/app/autolinker/client/index.js new file mode 100644 index 000000000000..d99e4ed77352 --- /dev/null +++ b/app/autolinker/client/index.js @@ -0,0 +1 @@ +import './client'; diff --git a/app/autolinker/server/index.js b/app/autolinker/server/index.js new file mode 100644 index 000000000000..97097791afdc --- /dev/null +++ b/app/autolinker/server/index.js @@ -0,0 +1 @@ +import './settings'; diff --git a/app/autolinker/server/settings.js b/app/autolinker/server/settings.js new file mode 100644 index 000000000000..81e65d05c0d2 --- /dev/null +++ b/app/autolinker/server/settings.js @@ -0,0 +1,20 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../settings'; + +Meteor.startup(function() { + const enableQuery = { + _id: 'AutoLinker', + value: true, + }; + + settings.add('AutoLinker', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nLabel: 'Enabled' }); + + settings.add('AutoLinker_StripPrefix', false, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_StripPrefix_Description', enableQuery }); + settings.add('AutoLinker_Urls_Scheme', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settings.add('AutoLinker_Urls_www', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settings.add('AutoLinker_Urls_TLD', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settings.add('AutoLinker_UrlsRegExp', '(://|www\\.).+', { type: 'string', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settings.add('AutoLinker_Email', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, enableQuery }); + settings.add('AutoLinker_Phone', true, { type: 'boolean', group: 'Message', section: 'AutoLinker', public: true, i18nDescription: 'AutoLinker_Phone_Description', enableQuery }); +}); diff --git a/packages/rocketchat-autotranslate/README.md b/app/autotranslate/README.md similarity index 100% rename from packages/rocketchat-autotranslate/README.md rename to app/autotranslate/README.md diff --git a/app/autotranslate/client/index.js b/app/autotranslate/client/index.js new file mode 100644 index 000000000000..c1deaf68e645 --- /dev/null +++ b/app/autotranslate/client/index.js @@ -0,0 +1,6 @@ +import './lib/actionButton'; +import './lib/tabBar'; +import './views/autoTranslateFlexTab.html'; +import './views/autoTranslateFlexTab'; + +export { AutoTranslate } from './lib/autotranslate'; diff --git a/app/autotranslate/client/lib/actionButton.js b/app/autotranslate/client/lib/actionButton.js new file mode 100644 index 000000000000..d80d669f81b7 --- /dev/null +++ b/app/autotranslate/client/lib/actionButton.js @@ -0,0 +1,68 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + +import { AutoTranslate } from './autotranslate'; +import { settings } from '../../../settings'; +import { hasAtLeastOnePermission } from '../../../authorization'; +import { MessageAction } from '../../../ui-utils'; +import { messageArgs } from '../../../ui-utils/client/lib/messageArgs'; +import { Messages } from '../../../models'; + +Meteor.startup(function() { + Tracker.autorun(function() { + if (settings.get('AutoTranslate_Enabled') && hasAtLeastOnePermission(['auto-translate'])) { + MessageAction.addButton({ + id: 'translate', + icon: 'language', + label: 'Translate', + context: [ + 'message', + 'message-mobile', + 'threads', + ], + action() { + const { msg: message } = messageArgs(this); + const language = AutoTranslate.getLanguage(message.rid); + if (!message.translations || !message.translations[language]) { // } && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) { + AutoTranslate.messageIdsToWait[message._id] = true; + Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + Meteor.call('autoTranslate.translateMessage', message, language); + } + const action = message.autoTranslateShowInverse ? '$unset' : '$set'; + Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); + }, + condition({ msg, u }) { + return msg && msg.u && msg.u._id !== u._id && msg.translations && !msg.translations.original; + }, + order: 90, + }); + MessageAction.addButton({ + id: 'view-original', + icon: 'language', + label: 'View_original', + context: [ + 'message', + 'message-mobile', + 'threads', + ], + action() { + const { msg: message } = messageArgs(this); + const language = AutoTranslate.getLanguage(message.rid); + if (!message.translations || !message.translations[language]) { // } && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) { + AutoTranslate.messageIdsToWait[message._id] = true; + Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + Meteor.call('autoTranslate.translateMessage', message, language); + } + const action = message.autoTranslateShowInverse ? '$unset' : '$set'; + Messages.update({ _id: message._id }, { [action]: { autoTranslateShowInverse: true } }); + }, + condition({ msg, u }) { + return msg && msg.u && msg.u._id !== u._id && msg.translations && msg.translations.original; + }, + order: 90, + }); + } else { + MessageAction.removeButton('toggle-language'); + } + }); +}); diff --git a/app/autotranslate/client/lib/autotranslate.js b/app/autotranslate/client/lib/autotranslate.js new file mode 100644 index 000000000000..74d81891755f --- /dev/null +++ b/app/autotranslate/client/lib/autotranslate.js @@ -0,0 +1,128 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; +import _ from 'underscore'; +import mem from 'mem'; + +import { Subscriptions, Messages } from '../../../models'; +import { callbacks } from '../../../callbacks'; +import { settings } from '../../../settings'; +import { hasAtLeastOnePermission } from '../../../authorization'; +import { CachedCollectionManager } from '../../../ui-cached-collection'; + +let userLanguage = 'en'; +let username = ''; + +Meteor.startup(() => Tracker.autorun(() => { + const user = Meteor.user(); + if (!user) { + return; + } + userLanguage = user.language || 'en'; + username = user.username; +})); + +export const AutoTranslate = { + findSubscriptionByRid: mem((rid) => Subscriptions.findOne({ rid })), + messageIdsToWait: {}, + supportedLanguages: [], + + getLanguage(rid) { + let subscription = {}; + if (rid) { + subscription = this.findSubscriptionByRid(rid); + } + const language = (subscription && subscription.autoTranslateLanguage) || userLanguage || window.defaultUserLanguage(); + if (language.indexOf('-') !== -1) { + if (!_.findWhere(this.supportedLanguages, { language })) { + return language.substr(0, 2); + } + } + return language; + }, + + translateAttachments(attachments, language) { + for (const attachment of attachments) { + if (attachment.author_name !== username) { + if (attachment.text && attachment.translations && attachment.translations[language]) { + attachment.text = attachment.translations[language]; + } + + if (attachment.description && attachment.translations && attachment.translations[language]) { + attachment.description = attachment.translations[language]; + } + + if (attachment.attachments && attachment.attachments.length > 0) { + attachment.attachments = this.translateAttachments(attachment.attachments, language); + } + } + } + return attachments; + }, + + init() { + Meteor.call('autoTranslate.getSupportedLanguages', 'en', (err, languages) => { + this.supportedLanguages = languages || []; + }); + + Tracker.autorun(() => { + Subscriptions.find().observeChanges({ + changed: (id, fields) => { + if (fields.hasOwnProperty('autoTranslate')) { + mem.clear(this.findSubscriptionByRid); + } + }, + }); + }); + + Tracker.autorun(() => { + if (settings.get('AutoTranslate_Enabled') && hasAtLeastOnePermission(['auto-translate'])) { + callbacks.add('renderMessage', (message) => { + const subscription = this.findSubscriptionByRid(message.rid); + const autoTranslateLanguage = this.getLanguage(message.rid); + if (message.u && message.u._id !== Meteor.userId()) { + if (!message.translations) { + message.translations = {}; + } + if (!!(subscription && subscription.autoTranslate) !== !!message.autoTranslateShowInverse) { + message.translations.original = message.html; + if (message.translations[autoTranslateLanguage]) { + message.html = message.translations[autoTranslateLanguage]; + } + + if (message.attachments && message.attachments.length > 0) { + message.attachments = this.translateAttachments(message.attachments, autoTranslateLanguage); + } + } + } else if (message.attachments && message.attachments.length > 0) { + message.attachments = this.translateAttachments(message.attachments, autoTranslateLanguage); + } + return message; + }, callbacks.priority.HIGH - 3, 'autotranslate'); + + callbacks.add('streamMessage', (message) => { + if (message.u && message.u._id !== Meteor.userId()) { + const subscription = this.findSubscriptionByRid(message.rid); + const language = this.getLanguage(message.rid); + if (subscription && subscription.autoTranslate === true && ((message.msg && (!message.translations || !message.translations[language])))) { // || (message.attachments && !_.find(message.attachments, attachment => { return attachment.translations && attachment.translations[language]; })) + Messages.update({ _id: message._id }, { $set: { autoTranslateFetching: true } }); + } else if (this.messageIdsToWait[message._id] !== undefined && subscription && subscription.autoTranslate !== true) { + Messages.update({ _id: message._id }, { $set: { autoTranslateShowInverse: true }, $unset: { autoTranslateFetching: true } }); + delete this.messageIdsToWait[message._id]; + } else if (message.autoTranslateFetching === true) { + Messages.update({ _id: message._id }, { $unset: { autoTranslateFetching: true } }); + } + } + }, callbacks.priority.HIGH - 3, 'autotranslate-stream'); + } else { + callbacks.remove('renderMessage', 'autotranslate'); + callbacks.remove('streamMessage', 'autotranslate-stream'); + } + }); + }, +}; + +Meteor.startup(function() { + CachedCollectionManager.onLogin(() => { + AutoTranslate.init(); + }); +}); diff --git a/app/autotranslate/client/lib/tabBar.js b/app/autotranslate/client/lib/tabBar.js new file mode 100644 index 000000000000..f527022009aa --- /dev/null +++ b/app/autotranslate/client/lib/tabBar.js @@ -0,0 +1,22 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + +import { settings } from '../../../settings'; +import { hasAtLeastOnePermission } from '../../../authorization'; +import { TabBar } from '../../../ui-utils'; + +Meteor.startup(function() { + Tracker.autorun(function() { + if (settings.get('AutoTranslate_Enabled') && hasAtLeastOnePermission(['auto-translate'])) { + return TabBar.addButton({ + groups: ['channel', 'group', 'direct'], + id: 'autotranslate', + i18nTitle: 'Auto_Translate', + icon: 'language', + template: 'autoTranslateFlexTab', + order: 20, + }); + } + TabBar.removeButton('autotranslate'); + }); +}); diff --git a/packages/rocketchat-autotranslate/client/stylesheets/autotranslate.css b/app/autotranslate/client/stylesheets/autotranslate.css similarity index 100% rename from packages/rocketchat-autotranslate/client/stylesheets/autotranslate.css rename to app/autotranslate/client/stylesheets/autotranslate.css diff --git a/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.html b/app/autotranslate/client/views/autoTranslateFlexTab.html similarity index 100% rename from packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.html rename to app/autotranslate/client/views/autoTranslateFlexTab.html diff --git a/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js b/app/autotranslate/client/views/autoTranslateFlexTab.js similarity index 87% rename from packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js rename to app/autotranslate/client/views/autoTranslateFlexTab.js index 47f3feb7d8c0..3add779473b3 100644 --- a/packages/rocketchat-autotranslate/client/views/autoTranslateFlexTab.js +++ b/app/autotranslate/client/views/autoTranslateFlexTab.js @@ -1,7 +1,13 @@ -/* globals ChatSubscription */ +import { Meteor } from 'meteor/meteor'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Random } from 'meteor/random'; +import { Template } from 'meteor/templating'; import _ from 'underscore'; import toastr from 'toastr'; +import { ChatSubscription, Subscriptions, Messages } from '../../../models'; +import { t, handleError } from '../../../utils'; + Template.autoTranslateFlexTab.helpers({ autoTranslate() { const sub = ChatSubscription.findOne({ @@ -38,7 +44,7 @@ Template.autoTranslateFlexTab.helpers({ let language = _.findWhere(supportedLanguages, { language: autoTranslateLanguage }); if (language) { return language.language; - } else if (autoTranslateLanguage.indexOf('-') !== -1) { + } if (autoTranslateLanguage.indexOf('-') !== -1) { language = _.findWhere(supportedLanguages, { language: autoTranslateLanguage.substr(0, 2) }); return language && language.language; } @@ -58,7 +64,7 @@ Template.autoTranslateFlexTab.helpers({ let language = _.findWhere(supportedLanguages, { language: targetLanguage }); if (language) { return language.name; - } else if (targetLanguage.indexOf('-') !== -1) { + } if (targetLanguage.indexOf('-') !== -1) { language = _.findWhere(supportedLanguages, { language: targetLanguage.substr(0, 2) }); return language && language.name; } @@ -92,7 +98,7 @@ Template.autoTranslateFlexTab.onCreated(function() { this.saveSetting = () => { const field = this.editing.get(); - const subscription = RocketChat.models.Subscriptions.findOne({ rid: this.rid, 'u._id': Meteor.userId() }); + const subscription = Subscriptions.findOne({ rid: this.rid, 'u._id': Meteor.userId() }); const previousLanguage = subscription.autoTranslateLanguage; let value; switch (field) { @@ -118,7 +124,7 @@ Template.autoTranslateFlexTab.onCreated(function() { } if (field === 'autoTranslate' && value === '0') { - RocketChat.models.Messages.update(query, { $unset: { autoTranslateShowInverse: 1 } }, { multi: true }); + Messages.update(query, { $unset: { autoTranslateShowInverse: 1 } }, { multi: true }); } const display = field === 'autoTranslate' ? true : subscription && subscription.autoTranslate; @@ -128,7 +134,7 @@ Template.autoTranslateFlexTab.onCreated(function() { query.autoTranslateShowInverse = true; } - RocketChat.models.Messages.update(query, { $set: { random: Random.id() } }, { multi: true }); + Messages.update(query, { $set: { random: Random.id() } }, { multi: true }); this.editing.set(); }); diff --git a/app/autotranslate/server/autotranslate.js b/app/autotranslate/server/autotranslate.js new file mode 100644 index 000000000000..13c77caf4c94 --- /dev/null +++ b/app/autotranslate/server/autotranslate.js @@ -0,0 +1,263 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; +import _ from 'underscore'; +import s from 'underscore.string'; + +import { settings } from '../../settings'; +import { callbacks } from '../../callbacks'; +import { Subscriptions, Messages } from '../../models'; +import { Markdown } from '../../markdown/server'; + +class AutoTranslate { + constructor() { + this.languages = []; + this.enabled = settings.get('AutoTranslate_Enabled'); + this.apiKey = settings.get('AutoTranslate_GoogleAPIKey'); + this.supportedLanguages = {}; + callbacks.add('afterSaveMessage', this.translateMessage.bind(this), callbacks.priority.MEDIUM, 'AutoTranslate'); + + settings.get('AutoTranslate_Enabled', (key, value) => { + this.enabled = value; + }); + settings.get('AutoTranslate_GoogleAPIKey', (key, value) => { + this.apiKey = value; + }); + } + + tokenize(message) { + if (!message.tokens || !Array.isArray(message.tokens)) { + message.tokens = []; + } + message = this.tokenizeEmojis(message); + message = this.tokenizeCode(message); + message = this.tokenizeURLs(message); + message = this.tokenizeMentions(message); + return message; + } + + tokenizeEmojis(message) { + let count = message.tokens.length; + message.msg = message.msg.replace(/:[+\w\d]+:/g, function(match) { + const token = `{${ count++ }}`; + message.tokens.push({ + token, + text: match, + }); + return token; + }); + + return message; + } + + tokenizeURLs(message) { + let count = message.tokens.length; + + const schemes = settings.get('Markdown_SupportSchemesForLink').split(',').join('|'); + + // Support ![alt text](http://image url) and [text](http://link) + message.msg = message.msg.replace(new RegExp(`(!?\\[)([^\\]]+)(\\]\\((?:${ schemes }):\\/\\/[^\\)]+\\))`, 'gm'), function(match, pre, text, post) { + const pretoken = `{${ count++ }}`; + message.tokens.push({ + token: pretoken, + text: pre, + }); + + const posttoken = `{${ count++ }}`; + message.tokens.push({ + token: posttoken, + text: post, + }); + + return pretoken + text + posttoken; + }); + + // Support + message.msg = message.msg.replace(new RegExp(`((?:<|<)(?:${ schemes }):\\/\\/[^\\|]+\\|)(.+?)(?=>|>)((?:>|>))`, 'gm'), function(match, pre, text, post) { + const pretoken = `{${ count++ }}`; + message.tokens.push({ + token: pretoken, + text: pre, + }); + + const posttoken = `{${ count++ }}`; + message.tokens.push({ + token: posttoken, + text: post, + }); + + return pretoken + text + posttoken; + }); + + return message; + } + + tokenizeCode(message) { + let count = message.tokens.length; + + message.html = message.msg; + message = Markdown.parseMessageNotEscaped(message); + message.msg = message.html; + + for (const tokenIndex in message.tokens) { + if (message.tokens.hasOwnProperty(tokenIndex)) { + const { token } = message.tokens[tokenIndex]; + if (token.indexOf('notranslate') === -1) { + const newToken = `{${ count++ }}`; + message.msg = message.msg.replace(token, newToken); + message.tokens[tokenIndex].token = newToken; + } + } + } + + return message; + } + + tokenizeMentions(message) { + let count = message.tokens.length; + + if (message.mentions && message.mentions.length > 0) { + message.mentions.forEach((mention) => { + message.msg = message.msg.replace(new RegExp(`(@${ mention.username })`, 'gm'), (match) => { + const token = `{${ count++ }}`; + message.tokens.push({ + token, + text: match, + }); + return token; + }); + }); + } + + if (message.channels && message.channels.length > 0) { + message.channels.forEach((channel) => { + message.msg = message.msg.replace(new RegExp(`(#${ channel.name })`, 'gm'), (match) => { + const token = `{${ count++ }}`; + message.tokens.push({ + token, + text: match, + }); + return token; + }); + }); + } + + return message; + } + + deTokenize(message) { + if (message.tokens && message.tokens.length > 0) { + for (const { token, text, noHtml } of message.tokens) { + message.msg = message.msg.replace(token, () => noHtml || text); + } + } + return message.msg; + } + + translateMessage(message, room, targetLanguage) { + if (this.enabled && this.apiKey) { + let targetLanguages; + if (targetLanguage) { + targetLanguages = [targetLanguage]; + } else { + targetLanguages = Subscriptions.getAutoTranslateLanguagesByRoomAndNotUser(room._id, message.u && message.u._id); + } + if (message.msg) { + Meteor.defer(() => { + const translations = {}; + let targetMessage = Object.assign({}, message); + + targetMessage.html = s.escapeHTML(String(targetMessage.msg)); + targetMessage = this.tokenize(targetMessage); + + let msgs = targetMessage.msg.split('\n'); + msgs = msgs.map((msg) => encodeURIComponent(msg)); + const query = `q=${ msgs.join('&q=') }`; + + const supportedLanguages = this.getSupportedLanguages('en'); + targetLanguages.forEach((language) => { + if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { + language = language.substr(0, 2); + } + let result; + try { + result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query }); + } catch (e) { + console.log('Error translating message', e); + return message; + } + if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) { + const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n'); + translations[language] = this.deTokenize(Object.assign({}, targetMessage, { msg: txt })); + } + }); + if (!_.isEmpty(translations)) { + Messages.addTranslations(message._id, translations); + } + }); + } + + if (message.attachments && message.attachments.length > 0) { + Meteor.defer(() => { + for (const index in message.attachments) { + if (message.attachments.hasOwnProperty(index)) { + const attachment = message.attachments[index]; + const translations = {}; + if (attachment.description || attachment.text) { + const query = `q=${ encodeURIComponent(attachment.description || attachment.text) }`; + const supportedLanguages = this.getSupportedLanguages('en'); + targetLanguages.forEach((language) => { + if (language.indexOf('-') !== -1 && !_.findWhere(supportedLanguages, { language })) { + language = language.substr(0, 2); + } + const result = HTTP.get('https://translation.googleapis.com/language/translate/v2', { params: { key: this.apiKey, target: language }, query }); + if (result.statusCode === 200 && result.data && result.data.data && result.data.data.translations && Array.isArray(result.data.data.translations) && result.data.data.translations.length > 0) { + const txt = result.data.data.translations.map((translation) => translation.translatedText).join('\n'); + translations[language] = txt; + } + }); + if (!_.isEmpty(translations)) { + Messages.addAttachmentTranslations(message._id, index, translations); + } + } + } + } + }); + } + } + return message; + } + + getSupportedLanguages(target) { + if (this.enabled && this.apiKey) { + if (this.supportedLanguages[target]) { + return this.supportedLanguages[target]; + } + + let result; + const params = { key: this.apiKey }; + if (target) { + params.target = target; + } + + if (this.supportedLanguages[target]) { + return this.supportedLanguages[target]; + } + + try { + result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params }); + } catch (e) { + if (e.response && e.response.statusCode === 400 && e.response.data && e.response.data.error && e.response.data.error.status === 'INVALID_ARGUMENT') { + params.target = 'en'; + target = 'en'; + if (!this.supportedLanguages[target]) { + result = HTTP.get('https://translation.googleapis.com/language/translate/v2/languages', { params }); + } + } + } + this.supportedLanguages[target || 'en'] = result && result.data && result.data.data && result.data.data.languages; + return this.supportedLanguages[target || 'en']; + } + } +} + +export default new AutoTranslate(); diff --git a/app/autotranslate/server/index.js b/app/autotranslate/server/index.js new file mode 100644 index 000000000000..a3692f29c722 --- /dev/null +++ b/app/autotranslate/server/index.js @@ -0,0 +1,6 @@ +import './settings'; +import './permissions'; +import './autotranslate'; +import './methods/getSupportedLanguages'; +import './methods/saveSettings'; +import './methods/translateMessage'; diff --git a/app/autotranslate/server/methods/getSupportedLanguages.js b/app/autotranslate/server/methods/getSupportedLanguages.js new file mode 100644 index 000000000000..2f44877a7e1c --- /dev/null +++ b/app/autotranslate/server/methods/getSupportedLanguages.js @@ -0,0 +1,23 @@ +import { Meteor } from 'meteor/meteor'; +import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; + +import { hasPermission } from '../../../authorization'; +import AutoTranslate from '../autotranslate'; + +Meteor.methods({ + 'autoTranslate.getSupportedLanguages'(targetLanguage) { + if (!hasPermission(Meteor.userId(), 'auto-translate')) { + throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { method: 'autoTranslate.saveSettings' }); + } + + return AutoTranslate.getSupportedLanguages(targetLanguage); + }, +}); + +DDPRateLimiter.addRule({ + type: 'method', + name: 'autoTranslate.getSupportedLanguages', + userId(/* userId*/) { + return true; + }, +}, 5, 60000); diff --git a/app/autotranslate/server/methods/saveSettings.js b/app/autotranslate/server/methods/saveSettings.js new file mode 100644 index 000000000000..c7a9adcc208a --- /dev/null +++ b/app/autotranslate/server/methods/saveSettings.js @@ -0,0 +1,44 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { hasPermission } from '../../../authorization'; +import { Subscriptions } from '../../../models'; + +Meteor.methods({ + 'autoTranslate.saveSettings'(rid, field, value, options) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'saveAutoTranslateSettings' }); + } + + if (!hasPermission(Meteor.userId(), 'auto-translate')) { + throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { method: 'autoTranslate.saveSettings' }); + } + + check(rid, String); + check(field, String); + check(value, String); + + if (['autoTranslate', 'autoTranslateLanguage'].indexOf(field) === -1) { + throw new Meteor.Error('error-invalid-settings', 'Invalid settings field', { method: 'saveAutoTranslateSettings' }); + } + + const subscription = Subscriptions.findOneByRoomIdAndUserId(rid, Meteor.userId()); + if (!subscription) { + throw new Meteor.Error('error-invalid-subscription', 'Invalid subscription', { method: 'saveAutoTranslateSettings' }); + } + + switch (field) { + case 'autoTranslate': + Subscriptions.updateAutoTranslateById(subscription._id, value === '1'); + if (!subscription.autoTranslateLanguage && options.defaultLanguage) { + Subscriptions.updateAutoTranslateLanguageById(subscription._id, options.defaultLanguage); + } + break; + case 'autoTranslateLanguage': + Subscriptions.updateAutoTranslateLanguageById(subscription._id, value); + break; + } + + return true; + }, +}); diff --git a/app/autotranslate/server/methods/translateMessage.js b/app/autotranslate/server/methods/translateMessage.js new file mode 100644 index 000000000000..26ea7cf301a1 --- /dev/null +++ b/app/autotranslate/server/methods/translateMessage.js @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; + +import { Rooms } from '../../../models'; +import AutoTranslate from '../autotranslate'; + +Meteor.methods({ + 'autoTranslate.translateMessage'(message, targetLanguage) { + const room = Rooms.findOneById(message && message.rid); + if (message && room && AutoTranslate) { + return AutoTranslate.translateMessage(message, room, targetLanguage); + } + }, +}); diff --git a/app/autotranslate/server/permissions.js b/app/autotranslate/server/permissions.js new file mode 100644 index 000000000000..64ce0028fa87 --- /dev/null +++ b/app/autotranslate/server/permissions.js @@ -0,0 +1,11 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../models'; + +Meteor.startup(() => { + if (Permissions) { + if (!Permissions.findOne({ _id: 'auto-translate' })) { + Permissions.insert({ _id: 'auto-translate', roles: ['admin'] }); + } + } +}); diff --git a/app/autotranslate/server/settings.js b/app/autotranslate/server/settings.js new file mode 100644 index 000000000000..02931fd38771 --- /dev/null +++ b/app/autotranslate/server/settings.js @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../settings'; + +Meteor.startup(function() { + settings.add('AutoTranslate_Enabled', false, { type: 'boolean', group: 'Message', section: 'AutoTranslate', public: true }); + settings.add('AutoTranslate_GoogleAPIKey', '', { type: 'string', group: 'Message', section: 'AutoTranslate', enableQuery: { _id: 'AutoTranslate_Enabled', value: true }, secret: true }); +}); diff --git a/app/bigbluebutton/index.js b/app/bigbluebutton/index.js new file mode 100644 index 000000000000..ba58589ba3d7 --- /dev/null +++ b/app/bigbluebutton/index.js @@ -0,0 +1 @@ +export { default } from './server/bigbluebutton-api'; diff --git a/packages/rocketchat-bigbluebutton/server/bigbluebutton-api.js b/app/bigbluebutton/server/bigbluebutton-api.js similarity index 100% rename from packages/rocketchat-bigbluebutton/server/bigbluebutton-api.js rename to app/bigbluebutton/server/bigbluebutton-api.js diff --git a/app/blockstack/client/index.js b/app/blockstack/client/index.js new file mode 100644 index 000000000000..cc52d4d8da97 --- /dev/null +++ b/app/blockstack/client/index.js @@ -0,0 +1,52 @@ +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; +import { check, Match } from 'meteor/check'; +import { Session } from 'meteor/session'; +import './routes'; + +const handleError = (error) => error && Session.set('errorMessage', error.reason || 'Unknown error'); + +// TODO: allow serviceConfig.loginStyle == popup +Meteor.loginWithBlockstack = (options, callback = handleError) => { + if (!options || !options.redirectURI) { + options = ServiceConfiguration.configurations.findOne({ + service: 'blockstack', + }); + + options.blockstackIDHost = Meteor.Device.isDesktop() + ? 'http://localhost:8888/auth' + : 'https://blockstack.org/auth'; + + options.scopes = ['store_write']; + } + + try { + check(options, Match.ObjectIncluding({ + blockstackIDHost: String, + redirectURI: String, + manifestURI: String, + })); + + import('blockstack/dist/blockstack').then(({ redirectToSignIn }) => + redirectToSignIn(options.redirectURI, options.manifestURI, options.scopes)); + } catch (err) { + callback.call(Meteor, err); + } +}; + +const meteorLogout = Meteor.logout; +Meteor.logout = (...args) => { + const serviceConfig = ServiceConfiguration.configurations.findOne({ + service: 'blockstack', + }); + + const blockstackAuth = Session.get('blockstack_auth'); + + if (serviceConfig && blockstackAuth) { + Session.delete('blockstack_auth'); + import('blockstack/dist/blockstack').then(({ signUserOut }) => + signUserOut(window.location.href)); + } + + return meteorLogout(...args); +}; diff --git a/app/blockstack/client/routes.js b/app/blockstack/client/routes.js new file mode 100644 index 000000000000..4791dce0c7d1 --- /dev/null +++ b/app/blockstack/client/routes.js @@ -0,0 +1,44 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import { FlowRouter } from 'meteor/kadira:flow-router'; + +const blockstackLogin = (authResponse, userData = {}) => { + Accounts.callLoginMethod({ + methodArguments: [{ + blockstack: true, + authResponse, + userData, + }], + userCallback() { + FlowRouter.go('home'); + }, + }); +}; + +FlowRouter.route('/_blockstack/validate', { + name: 'blockstackValidate', + async action(params, queryParams) { + const blockstack = await import('blockstack/dist/blockstack'); + + if (Meteor.userId()) { + console.log('Blockstack Auth requested when already logged in. Reloading.'); + return FlowRouter.go('home'); + } + + if (queryParams.authResponse == null) { + throw new Meteor.Error('Blockstack: Auth request without response param.'); + } + + let userData; + + if (blockstack.isUserSignedIn()) { + userData = blockstack.loadUserData(); + } + + if (blockstack.isSignInPending()) { + userData = await blockstack.handlePendingSignIn(); + } + + blockstackLogin(queryParams.authResponse, userData); + }, +}); diff --git a/packages/rocketchat-blockstack/server/main.js b/app/blockstack/server/index.js similarity index 100% rename from packages/rocketchat-blockstack/server/main.js rename to app/blockstack/server/index.js diff --git a/app/blockstack/server/logger.js b/app/blockstack/server/logger.js new file mode 100644 index 000000000000..e88f4df9bf1c --- /dev/null +++ b/app/blockstack/server/logger.js @@ -0,0 +1,3 @@ +import { Logger } from '../../logger'; + +export const logger = new Logger('Blockstack'); diff --git a/app/blockstack/server/loginHandler.js b/app/blockstack/server/loginHandler.js new file mode 100644 index 000000000000..de00f0d1ccf4 --- /dev/null +++ b/app/blockstack/server/loginHandler.js @@ -0,0 +1,54 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; + +import { updateOrCreateUser } from './userHandler'; +import { handleAccessToken } from './tokenHandler'; +import { logger } from './logger'; +import { settings } from '../../settings'; +import { Users } from '../../models'; +import { setUserAvatar } from '../../lib'; + +// Blockstack login handler, triggered by a blockstack authResponse in route +Accounts.registerLoginHandler('blockstack', (loginRequest) => { + if (!loginRequest.blockstack || !loginRequest.authResponse) { + return; + } + + if (!settings.get('Blockstack_Enable')) { + return; + } + + logger.debug('Processing login request', loginRequest); + + const auth = handleAccessToken(loginRequest); + + // TODO: Fix #9484 and re-instate usage of accounts helper + // const result = Accounts.updateOrCreateUserFromExternalService('blockstack', auth.serviceData, auth.options) + const result = updateOrCreateUser(auth.serviceData, auth.options); + logger.debug('User create/update result', result); + + // Ensure processing succeeded + if (result === undefined || result.userId === undefined) { + return { + type: 'blockstack', + error: new Meteor.Error(Accounts.LoginCancelledError.numericError, 'User creation failed from Blockstack response token'), + }; + } + + if (result.isNew) { + try { + const user = Users.findOneById(result.userId, { fields: { 'services.blockstack.image': 1, username: 1 } }); + if (user && user.services && user.services.blockstack && user.services.blockstack.image) { + Meteor.runAsUser(user._id, () => { + setUserAvatar(user, user.services.blockstack.image, undefined, 'url'); + }); + } + } catch (e) { + console.error(e); + } + } + + delete result.isNew; + + return result; +}); diff --git a/app/blockstack/server/routes.js b/app/blockstack/server/routes.js new file mode 100644 index 000000000000..ee4c7cc087df --- /dev/null +++ b/app/blockstack/server/routes.js @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; +import { WebApp } from 'meteor/webapp'; + +import { settings } from '../../settings'; +import { RocketChatAssets } from '../../assets'; + +WebApp.connectHandlers.use('/_blockstack/manifest', Meteor.bindEnvironment(function(req, res) { + const name = settings.get('Site_Name'); + const startUrl = Meteor.absoluteUrl(); + const description = settings.get('Blockstack_Auth_Description'); + const iconUrl = RocketChatAssets.getURL('Assets_favicon_192'); + + res.writeHead(200, { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }); + + res.end(`{ + "name": "${ name }", + "start_url": "${ startUrl }", + "description": "${ description }", + "icons": [{ + "src": "${ iconUrl }", + "sizes": "192x192", + "type": "image/png" + }] + }`); +})); diff --git a/app/blockstack/server/settings.js b/app/blockstack/server/settings.js new file mode 100644 index 000000000000..f5e79db64975 --- /dev/null +++ b/app/blockstack/server/settings.js @@ -0,0 +1,70 @@ +import _ from 'underscore'; +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; + +import { logger } from './logger'; +import { settings } from '../../settings'; + +const defaults = { + enable: false, + loginStyle: 'redirect', + generateUsername: false, + manifestURI: Meteor.absoluteUrl('_blockstack/manifest'), + redirectURI: Meteor.absoluteUrl('_blockstack/validate'), + authDescription: 'Rocket.Chat login', + buttonLabelText: 'Blockstack', + buttonColor: '#271132', + buttonLabelColor: '#ffffff', +}; + +Meteor.startup(() => { + settings.addGroup('Blockstack', function() { + this.add('Blockstack_Enable', defaults.enable, { + type: 'boolean', + i18nLabel: 'Enable', + }); + this.add('Blockstack_Auth_Description', defaults.authDescription, { + type: 'string', + }); + this.add('Blockstack_ButtonLabelText', defaults.buttonLabelText, { + type: 'string', + }); + this.add('Blockstack_Generate_Username', defaults.generateUsername, { + type: 'boolean', + }); + }); +}); + +// Helper to return all Blockstack settings +const getSettings = () => Object.assign({}, defaults, { + enable: settings.get('Blockstack_Enable'), + authDescription: settings.get('Blockstack_Auth_Description'), + buttonLabelText: settings.get('Blockstack_ButtonLabelText'), + generateUsername: settings.get('Blockstack_Generate_Username'), +}); + +const configureService = _.debounce(Meteor.bindEnvironment(() => { + const serviceConfig = getSettings(); + + if (!serviceConfig.enable) { + logger.debug('Blockstack not enabled', serviceConfig); + return ServiceConfiguration.configurations.remove({ + service: 'blockstack', + }); + } + + ServiceConfiguration.configurations.upsert({ + service: 'blockstack', + }, { + $set: serviceConfig, + }); + + logger.debug('Init Blockstack auth', serviceConfig); +}), 1000); + +// Add settings to auth provider configs on startup +Meteor.startup(() => { + settings.get(/^Blockstack_.+/, () => { + configureService(); + }); +}); diff --git a/packages/rocketchat-blockstack/server/tokenHandler.js b/app/blockstack/server/tokenHandler.js similarity index 100% rename from packages/rocketchat-blockstack/server/tokenHandler.js rename to app/blockstack/server/tokenHandler.js index 30686814b113..32cd176086fb 100644 --- a/packages/rocketchat-blockstack/server/tokenHandler.js +++ b/app/blockstack/server/tokenHandler.js @@ -1,8 +1,8 @@ import { decodeToken } from 'blockstack'; - import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; + import { logger } from './logger'; // Handler extracts data from JSON and tokenised reponse. diff --git a/packages/rocketchat-blockstack/server/userHandler.js b/app/blockstack/server/userHandler.js similarity index 95% rename from packages/rocketchat-blockstack/server/userHandler.js rename to app/blockstack/server/userHandler.js index 8e23773b2735..8078d5358c7d 100644 --- a/packages/rocketchat-blockstack/server/userHandler.js +++ b/app/blockstack/server/userHandler.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { RocketChat } from 'meteor/rocketchat:lib'; + import { logger } from './logger'; +import { generateUsernameSuggestion } from '../../lib'; // Updates or creates a user after we authenticate with Blockstack // Clones Accounts.updateOrCreateUserFromExternalService with some modifications @@ -53,7 +54,7 @@ export const updateOrCreateUser = (serviceData, options) => { if (profile.username && profile.username !== '') { newUser.username = profile.username; } else if (serviceConfig.generateUsername === true) { - newUser.username = RocketChat.generateUsernameSuggestion(newUser); + newUser.username = generateUsernameSuggestion(newUser); } // If no username at this point it will suggest one from the name diff --git a/packages/rocketchat-bot-helpers/README.md b/app/bot-helpers/README.md similarity index 100% rename from packages/rocketchat-bot-helpers/README.md rename to app/bot-helpers/README.md diff --git a/app/bot-helpers/index.js b/app/bot-helpers/index.js new file mode 100644 index 000000000000..f5778a23b606 --- /dev/null +++ b/app/bot-helpers/index.js @@ -0,0 +1 @@ +import './server/index'; diff --git a/app/bot-helpers/server/index.js b/app/bot-helpers/server/index.js new file mode 100644 index 000000000000..e88fef7641d0 --- /dev/null +++ b/app/bot-helpers/server/index.js @@ -0,0 +1,166 @@ +import './settings'; +import { Meteor } from 'meteor/meteor'; +import _ from 'underscore'; + +import { Users, Rooms } from '../../models'; +import { settings } from '../../settings'; +import { hasRole } from '../../authorization'; + +/** + * BotHelpers helps bots + * "private" properties use meteor collection cursors, so they stay reactive + * "public" properties use getters to fetch and filter collections as array + */ +class BotHelpers { + constructor() { + this.queries = { + online: { status: { $ne: 'offline' } }, + users: { roles: { $not: { $all: ['bot'] } } }, + }; + } + + // setup collection cursors with array of fields from setting + setupCursors(fieldsSetting) { + this.userFields = {}; + if (typeof fieldsSetting === 'string') { + fieldsSetting = fieldsSetting.split(','); + } + fieldsSetting.forEach((n) => { + this.userFields[n.trim()] = 1; + }); + this._allUsers = Users.find(this.queries.users, { fields: this.userFields }); + this._onlineUsers = Users.find({ $and: [this.queries.users, this.queries.online] }, { fields: this.userFields }); + } + + // request methods or props as arguments to Meteor.call + request(prop, ...params) { + if (typeof this[prop] === 'undefined') { + return null; + } if (typeof this[prop] === 'function') { + return this[prop](...params); + } + return this[prop]; + } + + addUserToRole(userName, roleName) { + Meteor.call('authorization:addUserToRole', roleName, userName); + } + + removeUserFromRole(userName, roleName) { + Meteor.call('authorization:removeUserFromRole', roleName, userName); + } + + addUserToRoom(userName, room) { + const foundRoom = Rooms.findOneByIdOrName(room); + + if (!_.isObject(foundRoom)) { + throw new Meteor.Error('invalid-channel'); + } + + const data = {}; + data.rid = foundRoom._id; + data.username = userName; + Meteor.call('addUserToRoom', data); + } + + removeUserFromRoom(userName, room) { + const foundRoom = Rooms.findOneByIdOrName(room); + + if (!_.isObject(foundRoom)) { + throw new Meteor.Error('invalid-channel'); + } + const data = {}; + data.rid = foundRoom._id; + data.username = userName; + Meteor.call('removeUserFromRoom', data); + } + + // generic error whenever property access insufficient to fill request + requestError() { + throw new Meteor.Error('error-not-allowed', 'Bot request not allowed', { method: 'botRequest', action: 'bot_request' }); + } + + // "public" properties accessed by getters + // allUsers / onlineUsers return whichever properties are enabled by settings + get allUsers() { + if (!Object.keys(this.userFields).length) { + this.requestError(); + return false; + } + return this._allUsers.fetch(); + } + + get onlineUsers() { + if (!Object.keys(this.userFields).length) { + this.requestError(); + return false; + } + return this._onlineUsers.fetch(); + } + + get allUsernames() { + if (!this.userFields.hasOwnProperty('username')) { + this.requestError(); + return false; + } + return this._allUsers.fetch().map((user) => user.username); + } + + get onlineUsernames() { + if (!this.userFields.hasOwnProperty('username')) { + this.requestError(); + return false; + } + return this._onlineUsers.fetch().map((user) => user.username); + } + + get allNames() { + if (!this.userFields.hasOwnProperty('name')) { + this.requestError(); + return false; + } + return this._allUsers.fetch().map((user) => user.name); + } + + get onlineNames() { + if (!this.userFields.hasOwnProperty('name')) { + this.requestError(); + return false; + } + return this._onlineUsers.fetch().map((user) => user.name); + } + + get allIDs() { + if (!this.userFields.hasOwnProperty('_id') || !this.userFields.hasOwnProperty('username')) { + this.requestError(); + return false; + } + return this._allUsers.fetch().map((user) => ({ id: user._id, name: user.username })); + } + + get onlineIDs() { + if (!this.userFields.hasOwnProperty('_id') || !this.userFields.hasOwnProperty('username')) { + this.requestError(); + return false; + } + return this._onlineUsers.fetch().map((user) => ({ id: user._id, name: user.username })); + } +} + +// add class to meteor methods +const botHelpers = new BotHelpers(); + +// init cursors with fields setting and update on setting change +settings.get('BotHelpers_userFields', function(settingKey, settingValue) { + botHelpers.setupCursors(settingValue); +}); + +Meteor.methods({ + botRequest: (...args) => { + const userID = Meteor.userId(); + if (userID && hasRole(userID, 'bot')) { + return botHelpers.request(...args); + } + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'botRequest' }); + }, +}); diff --git a/app/bot-helpers/server/settings.js b/app/bot-helpers/server/settings.js new file mode 100644 index 000000000000..dc4f5640c940 --- /dev/null +++ b/app/bot-helpers/server/settings.js @@ -0,0 +1,14 @@ +import { Meteor } from 'meteor/meteor'; + +import { settings } from '../../settings'; + +Meteor.startup(function() { + settings.addGroup('Bots', function() { + this.add('BotHelpers_userFields', '_id, name, username, emails, language, utcOffset', { + type: 'string', + section: 'Helpers', + i18nLabel: 'BotHelpers_userFields', + i18nDescription: 'BotHelpers_userFields_Description', + }); + }); +}); diff --git a/app/callbacks/client/index.js b/app/callbacks/client/index.js new file mode 100644 index 000000000000..486af6f60697 --- /dev/null +++ b/app/callbacks/client/index.js @@ -0,0 +1,5 @@ +import { callbacks } from '../lib/callbacks'; + +export { + callbacks, +}; diff --git a/app/callbacks/index.js b/app/callbacks/index.js new file mode 100644 index 000000000000..a67eca871efb --- /dev/null +++ b/app/callbacks/index.js @@ -0,0 +1,8 @@ +import { Meteor } from 'meteor/meteor'; + +if (Meteor.isClient) { + module.exports = require('./client/index.js'); +} +if (Meteor.isServer) { + module.exports = require('./server/index.js'); +} diff --git a/app/callbacks/lib/callbacks.js b/app/callbacks/lib/callbacks.js new file mode 100644 index 000000000000..193b6be06ed6 --- /dev/null +++ b/app/callbacks/lib/callbacks.js @@ -0,0 +1,149 @@ +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 +*/ + +export const callbacks = {}; + +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 +*/ + +callbacks.priority = { + HIGH: -1000, + MEDIUM: 0, + LOW: 1000, +}; + +const getHooks = (hookName) => callbacks[hookName] || []; + +/* +* Add a callback function to a hook +* @param {String} hook - The name of the hook +* @param {Function} callback - The callback function +*/ + +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; + callback.stack = new Error().stack; + + callbacks[hook].push(callback); + callbacks[hook] = _.sortBy(callbacks[hook], (callback) => callback.priority || callbacks.priority.MEDIUM); + combinedCallbacks.set(hook, create(hook, callbacks[hook])); +}; + + +/* +* Remove a callback from a hook +* @param {string} hook - The name of the hook +* @param {string} id - The callback's id +*/ + +callbacks.remove = function(hook, id) { + callbacks[hook] = getHooks(hook).filter((callback) => callback.id !== id); + combinedCallbacks.set(hook, create(hook, callbacks[hook])); +}; + +callbacks.runItem = ({ callback, result, constant /* , hook */ }) => callback(result, constant); + +/* +* Successively run all of a hook's callbacks on an item +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +* @returns {Object} Returns the item after it's been through all the callbacks for this hook +*/ + +callbacks.run = function(hook, item, constant) { + const runner = combinedCallbacks.get(hook); + if (!runner) { + return item; + } + + return runner(item, constant); + + // return callbackItems.reduce(function(result, callback) { + // const callbackResult = callbacks.runItem({ hook, callback, result, constant }); + + // return typeof callbackResult === 'undefined' ? result : callbackResult; + // }, item); +}; + + +/* +* Successively run all of a hook's callbacks on an item, in async mode (only works on server) +* @param {String} hook - The name of the hook +* @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks +* @param {Object} [constant] - An optional constant that will be passed along to each callback +*/ + +callbacks.runAsync = Meteor.isServer ? function(hook, item, constant) { + const callbackItems = callbacks[hook]; + 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/callbacks/server/index.js b/app/callbacks/server/index.js new file mode 100644 index 000000000000..486af6f60697 --- /dev/null +++ b/app/callbacks/server/index.js @@ -0,0 +1,5 @@ +import { callbacks } from '../lib/callbacks'; + +export { + callbacks, +}; diff --git a/app/cas/client/cas_client.js b/app/cas/client/cas_client.js new file mode 100644 index 000000000000..7046f4d024ed --- /dev/null +++ b/app/cas/client/cas_client.js @@ -0,0 +1,77 @@ +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import { Random } from 'meteor/random'; + +import { settings } from '../../settings'; + +const openCenteredPopup = function(url, width, height) { + const screenX = typeof window.screenX !== 'undefined' ? window.screenX : window.screenLeft; + const screenY = typeof window.screenY !== 'undefined' ? window.screenY : window.screenTop; + const outerWidth = typeof window.outerWidth !== 'undefined' ? window.outerWidth : document.body.clientWidth; + const outerHeight = typeof window.outerHeight !== 'undefined' ? window.outerHeight : document.body.clientHeight - 22; + // XXX what is the 22? + + // Use `outerWidth - width` and `outerHeight - height` for help in + // positioning the popup centered relative to the current window + const left = screenX + (outerWidth - width) / 2; + const top = screenY + (outerHeight - height) / 2; + const features = `width=${ width },height=${ height },left=${ left },top=${ top },scrollbars=yes`; + + const newwindow = window.open(url, 'Login', features); + if (newwindow.focus) { + newwindow.focus(); + } + + return newwindow; +}; + +Meteor.loginWithCas = function(options, callback) { + options = options || {}; + + const credentialToken = Random.id(); + const login_url = settings.get('CAS_login_url'); + const popup_width = settings.get('CAS_popup_width'); + const popup_height = settings.get('CAS_popup_height'); + + if (!login_url) { + return; + } + + const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + // check if the provided CAS URL already has some parameters + const delim = login_url.split('?').length > 1 ? '&' : '?'; + const loginUrl = `${ login_url }${ delim }service=${ appUrl }/_cas/${ credentialToken }`; + + const popup = openCenteredPopup( + loginUrl, + popup_width || 800, + popup_height || 600 + ); + + + const checkPopupOpen = setInterval(function() { + let popupClosed; + try { + // Fix for #328 - added a second test criteria (popup.closed === undefined) + // to humour this Android quirk: + // http://code.google.com/p/android/issues/detail?id=21061 + popupClosed = popup.closed || popup.closed === undefined; + } catch (e) { + // For some unknown reason, IE9 (and others?) sometimes (when + // the popup closes too quickly?) throws "SCRIPT16386: No such + // interface supported" when trying to read 'popup.closed'. Try + // again in 100ms. + return; + } + + if (popupClosed) { + clearInterval(checkPopupOpen); + + // check auth on server. + Accounts.callLoginMethod({ + methodArguments: [{ cas: { credentialToken } }], + userCallback: callback, + }); + } + }, 100); +}; diff --git a/app/cas/client/index.js b/app/cas/client/index.js new file mode 100644 index 000000000000..75213558d6d8 --- /dev/null +++ b/app/cas/client/index.js @@ -0,0 +1 @@ +import './cas_client'; diff --git a/app/cas/server/cas_rocketchat.js b/app/cas/server/cas_rocketchat.js new file mode 100644 index 000000000000..0a6ec78e520e --- /dev/null +++ b/app/cas/server/cas_rocketchat.js @@ -0,0 +1,69 @@ +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; + +import { Logger } from '../../logger'; +import { settings } from '../../settings'; + +export const logger = new Logger('CAS', {}); + +Meteor.startup(function() { + settings.addGroup('CAS', function() { + this.add('CAS_enabled', false, { type: 'boolean', group: 'CAS', public: true }); + this.add('CAS_base_url', '', { type: 'string', group: 'CAS', public: true }); + this.add('CAS_login_url', '', { type: 'string', group: 'CAS', public: true }); + this.add('CAS_version', '1.0', { type: 'select', values: [{ key: '1.0', i18nLabel: '1.0' }, { key: '2.0', i18nLabel: '2.0' }], group: 'CAS' }); + + this.section('Attribute_handling', function() { + // Enable/disable sync + this.add('CAS_Sync_User_Data_Enabled', true, { type: 'boolean' }); + // Attribute mapping table + this.add('CAS_Sync_User_Data_FieldMap', '{}', { type: 'string' }); + }); + + this.section('CAS_Login_Layout', function() { + this.add('CAS_popup_width', '810', { type: 'string', group: 'CAS', public: true }); + this.add('CAS_popup_height', '610', { type: 'string', group: 'CAS', public: true }); + this.add('CAS_button_label_text', 'CAS', { type: 'string', group: 'CAS' }); + this.add('CAS_button_label_color', '#FFFFFF', { type: 'color', group: 'CAS' }); + this.add('CAS_button_color', '#1d74f5', { type: 'color', group: 'CAS' }); + this.add('CAS_autoclose', true, { type: 'boolean', group: 'CAS' }); + }); + }); +}); + +let timer; + +function updateServices(/* record*/) { + if (typeof timer !== 'undefined') { + Meteor.clearTimeout(timer); + } + + timer = Meteor.setTimeout(function() { + const data = { + // These will pe passed to 'node-cas' as options + enabled: settings.get('CAS_enabled'), + base_url: settings.get('CAS_base_url'), + login_url: settings.get('CAS_login_url'), + // Rocketchat Visuals + buttonLabelText: settings.get('CAS_button_label_text'), + buttonLabelColor: settings.get('CAS_button_label_color'), + buttonColor: settings.get('CAS_button_color'), + width: settings.get('CAS_popup_width'), + height: settings.get('CAS_popup_height'), + autoclose: settings.get('CAS_autoclose'), + }; + + // Either register or deregister the CAS login service based upon its configuration + if (data.enabled) { + logger.info('Enabling CAS login service'); + ServiceConfiguration.configurations.upsert({ service: 'cas' }, { $set: data }); + } else { + logger.info('Disabling CAS login service'); + ServiceConfiguration.configurations.remove({ service: 'cas' }); + } + }, 2000); +} + +settings.get(/^CAS_.+/, (key, value) => { + updateServices(value); +}); diff --git a/app/cas/server/cas_server.js b/app/cas/server/cas_server.js new file mode 100644 index 000000000000..410fd4f16e06 --- /dev/null +++ b/app/cas/server/cas_server.js @@ -0,0 +1,268 @@ +import url from 'url'; + +import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; +import { Random } from 'meteor/random'; +import { WebApp } from 'meteor/webapp'; +import { RoutePolicy } from 'meteor/routepolicy'; +import _ from 'underscore'; +import fiber from 'fibers'; +import CAS from 'cas'; + +import { logger } from './cas_rocketchat'; +import { settings } from '../../settings'; +import { Rooms, Subscriptions, CredentialTokens } from '../../models'; +import { _setRealName } from '../../lib'; + +RoutePolicy.declare('/_cas/', 'network'); + +const closePopup = function(res) { + res.writeHead(200, { 'Content-Type': 'text/html' }); + const content = ''; + res.end(content, 'utf-8'); +}; + +const casTicket = function(req, token, callback) { + // get configuration + if (!settings.get('CAS_enabled')) { + logger.error('Got ticket validation request, but CAS is not enabled'); + callback(); + } + + // get ticket and validate. + const parsedUrl = url.parse(req.url, true); + const ticketId = parsedUrl.query.ticket; + const baseUrl = settings.get('CAS_base_url'); + const cas_version = parseFloat(settings.get('CAS_version')); + const appUrl = Meteor.absoluteUrl().replace(/\/$/, '') + __meteor_runtime_config__.ROOT_URL_PATH_PREFIX; + logger.debug(`Using CAS_base_url: ${ baseUrl }`); + + const cas = new CAS({ + base_url: baseUrl, + version: cas_version, + service: `${ appUrl }/_cas/${ token }`, + }); + + cas.validate(ticketId, Meteor.bindEnvironment(function(err, status, username, details) { + if (err) { + logger.error(`error when trying to validate: ${ err.message }`); + } else if (status) { + logger.info(`Validated user: ${ username }`); + const user_info = { username }; + + // CAS 2.0 attributes handling + if (details && details.attributes) { + _.extend(user_info, { attributes: details.attributes }); + } + CredentialTokens.create(token, user_info); + } else { + logger.error(`Unable to validate ticket: ${ ticketId }`); + } + // logger.debug("Receveied response: " + JSON.stringify(details, null , 4)); + + callback(); + })); +}; + +const middleware = function(req, res, next) { + // Make sure to catch any exceptions because otherwise we'd crash + // the runner + try { + const barePath = req.url.substring(0, req.url.indexOf('?')); + const splitPath = barePath.split('/'); + + // Any non-cas request will continue down the default + // middlewares. + if (splitPath[1] !== '_cas') { + next(); + return; + } + + // get auth token + const credentialToken = splitPath[2]; + if (!credentialToken) { + closePopup(res); + return; + } + + // validate ticket + casTicket(req, credentialToken, function() { + closePopup(res); + }); + } catch (err) { + logger.error(`Unexpected error : ${ err.message }`); + closePopup(res); + } +}; + +// Listen to incoming OAuth http requests +WebApp.connectHandlers.use(function(req, res, next) { + // Need to create a fiber since we're using synchronous http calls and nothing + // else is wrapping this in a fiber automatically + fiber(function() { + middleware(req, res, next); + }).run(); +}); + +/* + * Register a server-side login handle. + * It is call after Accounts.callLoginMethod() is call from client. + * + */ +Accounts.registerLoginHandler(function(options) { + if (!options.cas) { + return undefined; + } + + const credentials = CredentialTokens.findOneById(options.cas.credentialToken); + if (credentials === undefined) { + throw new Meteor.Error(Accounts.LoginCancelledError.numericError, + 'no matching login attempt found'); + } + + const result = credentials.userInfo; + const syncUserDataFieldMap = settings.get('CAS_Sync_User_Data_FieldMap').trim(); + const cas_version = parseFloat(settings.get('CAS_version')); + const sync_enabled = settings.get('CAS_Sync_User_Data_Enabled'); + + // We have these + const ext_attrs = { + username: result.username, + }; + + // We need these + const int_attrs = { + email: undefined, + name: undefined, + username: undefined, + rooms: undefined, + }; + + // Import response attributes + if (cas_version >= 2.0) { + // Clean & import external attributes + _.each(result.attributes, function(value, ext_name) { + if (value) { + ext_attrs[ext_name] = value[0]; + } + }); + } + + // Source internal attributes + if (syncUserDataFieldMap) { + // Our mapping table: key(int_attr) -> value(ext_attr) + // Spoken: Source this internal attribute from these external attributes + const attr_map = JSON.parse(syncUserDataFieldMap); + + _.each(attr_map, function(source, int_name) { + // Source is our String to interpolate + if (_.isString(source)) { + let replacedValue = source; + _.each(ext_attrs, function(value, ext_name) { + replacedValue = replacedValue.replace(`%${ ext_name }%`, ext_attrs[ext_name]); + }); + + if (source !== replacedValue) { + int_attrs[int_name] = replacedValue; + logger.debug(`Sourced internal attribute: ${ int_name } = ${ replacedValue }`); + } else { + logger.debug(`Sourced internal attribute: ${ int_name } skipped.`); + } + } + }); + } + + // Search existing user by its external service id + logger.debug(`Looking up user by id: ${ result.username }`); + // First, look for a user that has logged in from CAS with this username before + let user = Meteor.users.findOne({ 'services.cas.external_id': result.username }); + if (!user) { + // If that user was not found, check if there's any CAS user that is currently using that username on Rocket.Chat + // With this, CAS login will continue to work if the user is renamed on both sides and also if the user is renamed only on Rocket.Chat. + const username = new RegExp(`^${ result.username }$`, 'i'); + user = Meteor.users.findOne({ 'services.cas.external_id': { $exists: true }, username }); + if (user) { + // Update the user's external_id to reflect this new username. + Meteor.users.update(user, { $set: { 'services.cas.external_id': result.username } }); + } + } + + if (user) { + logger.debug(`Using existing user for '${ result.username }' with id: ${ user._id }`); + if (sync_enabled) { + logger.debug('Syncing user attributes'); + // Update name + if (int_attrs.name) { + _setRealName(user._id, int_attrs.name); + } + + // Update email + if (int_attrs.email) { + Meteor.users.update(user, { $set: { emails: [{ address: int_attrs.email, verified: true }] } }); + } + } + } else { + // Define new user + const newUser = { + username: result.username, + active: true, + globalRoles: ['user'], + emails: [], + services: { + cas: { + external_id: result.username, + version: cas_version, + attrs: int_attrs, + }, + }, + }; + + // Add User.name + if (int_attrs.name) { + _.extend(newUser, { + name: int_attrs.name, + }); + } + + // Add email + if (int_attrs.email) { + _.extend(newUser, { + emails: [{ address: int_attrs.email, verified: true }], + }); + } + + // Create the user + logger.debug(`User "${ result.username }" does not exist yet, creating it`); + const userId = Accounts.insertUserDoc({}, newUser); + + // Fetch and use it + user = Meteor.users.findOne(userId); + logger.debug(`Created new user for '${ result.username }' with id: ${ user._id }`); + // logger.debug(JSON.stringify(user, undefined, 4)); + + logger.debug(`Joining user to attribute channels: ${ int_attrs.rooms }`); + if (int_attrs.rooms) { + _.each(int_attrs.rooms.split(','), function(room_name) { + if (room_name) { + let room = Rooms.findOneByNameAndType(room_name, 'c'); + if (!room) { + room = Rooms.createWithIdTypeAndName(Random.id(), 'c', room_name); + } + + if (!Subscriptions.findOneByRoomIdAndUserId(room._id, userId)) { + Subscriptions.createWithRoomAndUser(room, user, { + ts: new Date(), + open: true, + alert: true, + unread: 1, + userMentions: 1, + groupMentions: 0, + }); + } + } + }); + } + } + + return { userId: user._id }; +}); diff --git a/app/cas/server/index.js b/app/cas/server/index.js new file mode 100644 index 000000000000..0ad22d77b198 --- /dev/null +++ b/app/cas/server/index.js @@ -0,0 +1,2 @@ +import './cas_rocketchat'; +import './cas_server'; diff --git a/app/channel-settings-mail-messages/client/index.js b/app/channel-settings-mail-messages/client/index.js new file mode 100644 index 000000000000..82a489dbed7e --- /dev/null +++ b/app/channel-settings-mail-messages/client/index.js @@ -0,0 +1,3 @@ +import './lib/startup'; +import './views/mailMessagesInstructions.html'; +import './views/mailMessagesInstructions'; diff --git a/app/channel-settings-mail-messages/client/lib/startup.js b/app/channel-settings-mail-messages/client/lib/startup.js new file mode 100644 index 000000000000..60986fc628be --- /dev/null +++ b/app/channel-settings-mail-messages/client/lib/startup.js @@ -0,0 +1,20 @@ +// import resetSelection from '../resetSelection'; +import { Meteor } from 'meteor/meteor'; + +import { TabBar } from '../../../ui-utils'; +import { hasAllPermission } from '../../../authorization'; + +Meteor.startup(() => { + TabBar.addButton({ + groups: ['channel', 'group', 'direct'], + id: 'mail-messages', + anonymous: true, + i18nTitle: 'Mail_Messages', + icon: 'mail', + template: 'mailMessagesInstructions', + order: 10, + condition: () => hasAllPermission('mail-messages'), + }); + + // RocketChat.callbacks.add('roomExit', () => resetSelection(false), RocketChat.callbacks.priority.MEDIUM, 'room-exit-mail-messages'); +}); diff --git a/packages/rocketchat-channel-settings-mail-messages/client/resetSelection.js b/app/channel-settings-mail-messages/client/resetSelection.js similarity index 89% rename from packages/rocketchat-channel-settings-mail-messages/client/resetSelection.js rename to app/channel-settings-mail-messages/client/resetSelection.js index a8493df545d0..ef62124d9c7c 100644 --- a/packages/rocketchat-channel-settings-mail-messages/client/resetSelection.js +++ b/app/channel-settings-mail-messages/client/resetSelection.js @@ -1,3 +1,5 @@ +import { Blaze } from 'meteor/blaze'; + export default function resetSelection(reset) { const [el] = $('.messages-box'); if (!el) { diff --git a/packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.html b/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.html similarity index 90% rename from packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.html rename to app/channel-settings-mail-messages/client/views/mailMessagesInstructions.html index 9cdfbe698a9b..6b79bef0a061 100644 --- a/packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.html +++ b/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.html @@ -3,7 +3,7 @@ {{#if selectedMessages}}
- {{> icon block="mail-messages__instructions-icon" icon="modal-success"}} + {{> icon block="mail-messages__instructions-icon rc-icon--default-size" icon="checkmark-circled"}}
{{selectedMessages.length}} Messages selected Click here to clear the selection @@ -13,7 +13,7 @@ {{else}}
- {{> icon block="mail-messages__instructions-icon" icon="hand-pointer"}} + {{> icon block="mail-messages__instructions-icon rc-icon--default-size" icon="hand-pointer"}}
{{_ "Click_the_messages_you_would_like_to_send_by_email"}}
@@ -25,7 +25,7 @@
{{_ "To_users"}}
- {{> icon block="rc-input__icon-svg" icon="at"}} + {{> icon block="rc-input__icon-svg rc-icon--default-size" icon="at"}}
{{#each user in selectedUsers}} @@ -36,9 +36,7 @@
{{#with config}} {{#if autocomplete 'isShowing'}} - {{#if autocomplete 'isLoaded'}} - {{> popupList data=config items=items}} - {{/if}} + {{> popupList data=config items=items ready=(autocomplete 'isLoaded')}} {{/if}} {{/with}} @@ -48,7 +46,7 @@
{{_ "To_additional_emails"}}
- {{> icon block="rc-input__icon-svg" icon="mail"}} + {{> icon block="rc-input__icon-svg rc-icon--default-size" icon="mail"}}
{{#each selectedEmails}} @@ -64,7 +62,7 @@
{{_ "Subject"}}
- {{> icon block="rc-input__icon-svg" icon="edit"}} + {{> icon block="rc-input__icon-svg rc-icon--default-size" icon="edit"}}
diff --git a/packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.js b/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js similarity index 86% rename from packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.js rename to app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js index 3fd4e2a39ded..a141dc7bc5af 100644 --- a/packages/rocketchat-channel-settings-mail-messages/client/views/mailMessagesInstructions.js +++ b/app/channel-settings-mail-messages/client/views/mailMessagesInstructions.js @@ -1,33 +1,19 @@ -/* global AutoComplete Deps */ +import { Meteor } from 'meteor/meteor'; +import { ReactiveVar } from 'meteor/reactive-var'; +import { Blaze } from 'meteor/blaze'; +import { Session } from 'meteor/session'; +import { Template } from 'meteor/templating'; +import { AutoComplete } from 'meteor/mizzao:autocomplete'; +import { Deps } from 'meteor/deps'; import toastr from 'toastr'; -import resetSelection from '../resetSelection'; -/* - * Code from https://github.com/dleitee/valid.js - * Checks for email - * @params email - * @return boolean - */ -const isEmail = (email) => { - const sQtext = '[^\\x0d\\x22\\x5c]'; - const sDtext = '[^\\x0d\\x5b-\\x5d]'; - const sAtom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d]+'; - const sQuotedPair = '\\x5c[\\x00-\\x7f]'; - const sDomainLiteral = `\\x5b(${ sDtext }|${ sQuotedPair })*\\x5d`; - const sQuotedString = `\\x22(${ sQtext }|${ sQuotedPair })*\\x22`; - const sDomainRef = sAtom; - const sSubDomain = `(${ sDomainRef }|${ sDomainLiteral })`; - const sWord = `(${ sAtom }|${ sQuotedString })`; - const sDomain = `${ sSubDomain }(\\x2e${ sSubDomain })*`; - const sLocalPart = `${ sWord }(\\x2e${ sWord })*`; - const sAddrSpec = `${ sLocalPart }\\x40${ sDomain }`; - const sValidEmail = `^${ sAddrSpec }$`; - const reg = new RegExp(sValidEmail); - return reg.test(email); -}; +import { ChatRoom } from '../../../models'; +import { t, isEmail, handleError, roomTypes } from '../../../utils'; +import { settings } from '../../../settings'; +import resetSelection from '../resetSelection'; const filterNames = (old) => { - const reg = new RegExp(`^${ RocketChat.settings.get('UTF8_Names_Validation') }$`); + const reg = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`); return [...old.replace(' ', '').toLocaleLowerCase()].filter((f) => reg.test(f)).join(''); }; @@ -41,7 +27,7 @@ Template.mailMessagesInstructions.helpers({ }, roomName() { const room = ChatRoom.findOne(Session.get('openedRoom')); - return room && room.name; + return room && roomTypes.getRoomName(room.t, room); }, erroredEmails() { const instance = Template.instance(); @@ -250,18 +236,18 @@ Template.mailMessagesInstructions.onCreated(function() { this.selectedUsers = new ReactiveVar([]); this.userFilter = new ReactiveVar(''); - const filter = { exceptions :[Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username)) }; + const filter = { exceptions: [Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username)) }; Deps.autorun(() => { filter.exceptions = [Meteor.user().username].concat(this.selectedUsers.get().map((u) => u.username)); }); this.ac = new AutoComplete( { - selector:{ + selector: { item: '.rc-popup-list__item', container: '.rc-popup-list__list', }, - + position: 'fixed', limit: 10, inputDelay: 300, rules: [ diff --git a/app/channel-settings-mail-messages/server/index.js b/app/channel-settings-mail-messages/server/index.js new file mode 100644 index 000000000000..465e185b386a --- /dev/null +++ b/app/channel-settings-mail-messages/server/index.js @@ -0,0 +1,2 @@ +import './lib/startup'; +import './methods/mailMessages'; diff --git a/app/channel-settings-mail-messages/server/lib/startup.js b/app/channel-settings-mail-messages/server/lib/startup.js new file mode 100644 index 000000000000..a04875ad90da --- /dev/null +++ b/app/channel-settings-mail-messages/server/lib/startup.js @@ -0,0 +1,13 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../../models'; + +Meteor.startup(function() { + const permission = { + _id: 'mail-messages', + roles: ['admin'], + }; + return Permissions.upsert(permission._id, { + $setOnInsert: permission, + }); +}); diff --git a/app/channel-settings-mail-messages/server/methods/mailMessages.js b/app/channel-settings-mail-messages/server/methods/mailMessages.js new file mode 100644 index 000000000000..d911210a25fc --- /dev/null +++ b/app/channel-settings-mail-messages/server/methods/mailMessages.js @@ -0,0 +1,92 @@ +import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; +import _ from 'underscore'; +import moment from 'moment'; + +import { hasPermission } from '../../../authorization'; +import { Users, Messages } from '../../../models'; +import { settings } from '../../../settings'; +import { Message } from '../../../ui-utils'; +import * as Mailer from '../../../mailer'; + +Meteor.methods({ + 'mailMessages'(data) { + const userId = Meteor.userId(); + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'mailMessages', + }); + } + check(data, Match.ObjectIncluding({ + rid: String, + to_users: [String], + to_emails: String, + subject: String, + messages: [String], + language: String, + })); + const room = Meteor.call('canAccessRoom', data.rid, userId); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'mailMessages', + }); + } + if (!hasPermission(userId, 'mail-messages')) { + throw new Meteor.Error('error-action-not-allowed', 'Mailing is not allowed', { + method: 'mailMessages', + action: 'Mailing', + }); + } + + const emails = _.compact(data.to_emails.trim().split(',')); + const missing = []; + if (data.to_users.length > 0) { + _.each(data.to_users, (username) => { + const user = Users.findOneByUsernameIgnoringCase(username); + if (user && user.emails && user.emails[0] && user.emails[0].address) { + emails.push(user.emails[0].address); + } else { + missing.push(username); + } + }); + } + _.each(emails, (email) => { + if (!Mailer.checkAddressFormat(email.trim())) { + throw new Meteor.Error('error-invalid-email', `Invalid email ${ email }`, { + method: 'mailMessages', + email, + }); + } + }); + const user = Meteor.user(); + const email = user.emails && user.emails[0] && user.emails[0].address; + data.language = data.language.split('-').shift().toLowerCase(); + if (data.language !== 'en') { + const localeFn = Meteor.call('loadLocale', data.language); + if (localeFn) { + Function(localeFn).call({ moment }); + moment.locale(data.language); + } + } + + const html = Messages.findByRoomIdAndMessageIds(data.rid, data.messages, { + sort: { ts: 1 }, + }).map(function(message) { + const dateTime = moment(message.ts).locale(data.language).format('L LT'); + return `

${ message.u.username } ${ dateTime }
${ Message.parse(message, data.language) }

`; + }).join(''); + + Mailer.send({ + to: emails, + from: settings.get('From_Email'), + replyTo: email, + subject: data.subject, + html, + }); + + return { + success: true, + missing, + }; + }, +}); diff --git a/app/channel-settings/client/index.js b/app/channel-settings/client/index.js new file mode 100644 index 000000000000..9834b106c9d9 --- /dev/null +++ b/app/channel-settings/client/index.js @@ -0,0 +1,7 @@ +import './startup/messageTypes'; +import './startup/tabBar'; +import './startup/trackSettingsChange'; +import './views/channelSettings.html'; +import './views/channelSettings'; + +export { ChannelSettings } from './lib/ChannelSettings'; diff --git a/packages/rocketchat-channel-settings/client/lib/ChannelSettings.js b/app/channel-settings/client/lib/ChannelSettings.js similarity index 88% rename from packages/rocketchat-channel-settings/client/lib/ChannelSettings.js rename to app/channel-settings/client/lib/ChannelSettings.js index 92ff8d7d0ef7..26467705ec47 100644 --- a/packages/rocketchat-channel-settings/client/lib/ChannelSettings.js +++ b/app/channel-settings/client/lib/ChannelSettings.js @@ -1,5 +1,8 @@ +import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; -RocketChat.ChannelSettings = new class { + +export const ChannelSettings = new class { constructor() { this.options = new ReactiveVar({}); } @@ -35,4 +38,4 @@ RocketChat.ChannelSettings = new class { }); return _.sortBy(allowedOptions, 'order'); } -}; +}(); diff --git a/app/channel-settings/client/startup/messageTypes.js b/app/channel-settings/client/startup/messageTypes.js new file mode 100644 index 000000000000..5b73ce3ffba3 --- /dev/null +++ b/app/channel-settings/client/startup/messageTypes.js @@ -0,0 +1,55 @@ +import { Meteor } from 'meteor/meteor'; +import s from 'underscore.string'; + +import { MessageTypes } from '../../../ui-utils'; +import { t } from '../../../utils'; + +Meteor.startup(function() { + MessageTypes.registerType({ + id: 'room_changed_privacy', + system: true, + message: 'room_changed_privacy', + data(message) { + return { + user_by: message.u && message.u.username, + room_type: t(message.msg), + }; + }, + }); + + MessageTypes.registerType({ + id: 'room_changed_topic', + system: true, + message: 'room_changed_topic', + data(message) { + return { + user_by: message.u && message.u.username, + room_topic: s.escapeHTML(message.msg || `(${ t('None').toLowerCase() })`), + }; + }, + }); + + MessageTypes.registerType({ + id: 'room_changed_announcement', + system: true, + message: 'room_changed_announcement', + data(message) { + return { + user_by: message.u && message.u.username, + room_announcement: s.escapeHTML(message.msg || `(${ t('None').toLowerCase() })`), + }; + }, + }); + + MessageTypes.registerType({ + id: 'room_changed_description', + system: true, + message: 'room_changed_description', + data(message) { + return { + user_by: message.u && message.u.username, + room_description: s.escapeHTML(message.msg || `(${ t('None').toLowerCase() })`), + }; + }, + }); +}); diff --git a/app/channel-settings/client/startup/tabBar.js b/app/channel-settings/client/startup/tabBar.js new file mode 100644 index 000000000000..2a3d794c4046 --- /dev/null +++ b/app/channel-settings/client/startup/tabBar.js @@ -0,0 +1,15 @@ +import { Meteor } from 'meteor/meteor'; + +import { TabBar } from '../../../ui-utils'; + +Meteor.startup(() => { + TabBar.addButton({ + groups: ['channel', 'group'], + id: 'channel-settings', + anonymous: true, + i18nTitle: 'Room_Info', + icon: 'info-circled', + template: 'channelSettings', + order: 1, + }); +}); diff --git a/app/channel-settings/client/startup/trackSettingsChange.js b/app/channel-settings/client/startup/trackSettingsChange.js new file mode 100644 index 000000000000..4dc05e611fc1 --- /dev/null +++ b/app/channel-settings/client/startup/trackSettingsChange.js @@ -0,0 +1,48 @@ +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Session } from 'meteor/session'; + +import { callbacks } from '../../../callbacks'; +import { RoomManager } from '../../../ui-utils'; +import { roomTypes } from '../../../utils'; +import { ChatRoom, ChatSubscription } from '../../../models'; + +Meteor.startup(function() { + const roomSettingsChangedCallback = (msg) => { + Tracker.nonreactive(() => { + if (msg.t === 'room_changed_privacy') { + if (Session.get('openedRoom') === msg.rid) { + const type = FlowRouter.current().route.name === 'channel' ? 'c' : 'p'; + RoomManager.close(type + FlowRouter.getParam('name')); + + const subscription = ChatSubscription.findOne({ rid: msg.rid }); + const route = subscription.t === 'c' ? 'channel' : 'group'; + FlowRouter.go(route, { name: subscription.name }, FlowRouter.current().queryParams); + } + } + }); + + return msg; + }; + + callbacks.add('streamMessage', roomSettingsChangedCallback, callbacks.priority.HIGH, 'room-settings-changed'); + + const roomNameChangedCallback = (msg) => { + Tracker.nonreactive(() => { + if (msg.t === 'r') { + if (Session.get('openedRoom') === msg.rid) { + const room = ChatRoom.findOne(msg.rid); + if (room.name !== FlowRouter.getParam('name')) { + RoomManager.close(room.t + FlowRouter.getParam('name')); + roomTypes.openRouteLink(room.t, room, FlowRouter.current().queryParams); + } + } + } + }); + + return msg; + }; + + callbacks.add('streamMessage', roomNameChangedCallback, callbacks.priority.HIGH, 'room-name-changed'); +}); diff --git a/packages/rocketchat-channel-settings/client/stylesheets/channel-settings.css b/app/channel-settings/client/stylesheets/channel-settings.css similarity index 98% rename from packages/rocketchat-channel-settings/client/stylesheets/channel-settings.css rename to app/channel-settings/client/stylesheets/channel-settings.css index dcf8e63bf85c..396bf68ed351 100644 --- a/packages/rocketchat-channel-settings/client/stylesheets/channel-settings.css +++ b/app/channel-settings/client/stylesheets/channel-settings.css @@ -1,4 +1,4 @@ -html.rtl .flex-tab { +.rtl .flex-tab { direction: rtl; & .channel-settings { diff --git a/packages/rocketchat-channel-settings/client/views/channelSettings.html b/app/channel-settings/client/views/channelSettings.html similarity index 95% rename from packages/rocketchat-channel-settings/client/views/channelSettings.html rename to app/channel-settings/client/views/channelSettings.html index 68e86137b573..32f93b538b39 100644 --- a/packages/rocketchat-channel-settings/client/views/channelSettings.html +++ b/app/channel-settings/client/views/channelSettings.html @@ -28,7 +28,7 @@
@@ -54,7 +54,7 @@
@@ -122,9 +122,9 @@ {{/with}}
- {{# if retentionEnabled settings.retentionEnabled.value.get }} + {{# if settings.retentionEnabled.value.get }} {{#with settings.retentionOverrideGlobal}}