diff --git a/.meteor/packages b/.meteor/packages index ddf589282d7..da66a3df2f9 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -64,6 +64,7 @@ rocketchat:emoji-custom rocketchat:emoji-emojione rocketchat:error-handler rocketchat:favico +rocketchat:federation rocketchat:file rocketchat:file-upload rocketchat:github-enterprise diff --git a/.meteor/versions b/.meteor/versions index ada8b493485..a249651b813 100644 --- a/.meteor/versions +++ b/.meteor/versions @@ -159,6 +159,7 @@ rocketchat:emoji-custom@1.0.0 rocketchat:emoji-emojione@0.0.1 rocketchat:error-handler@1.0.0 rocketchat:favico@0.0.1 +rocketchat:federation@0.0.1 rocketchat:file@0.0.1 rocketchat:file-upload@0.0.1 rocketchat:github-enterprise@0.0.1 diff --git a/imports/message-read-receipt/server/hooks.js b/imports/message-read-receipt/server/hooks.js index 22666d6df06..f9ffe22905c 100644 --- a/imports/message-read-receipt/server/hooks.js +++ b/imports/message-read-receipt/server/hooks.js @@ -15,3 +15,7 @@ callbacks.add('afterSaveMessage', (message, room) => { // mark message as read as well ReadReceipt.markMessageAsReadBySender(message, room._id, message.u._id); }); + +callbacks.add('afterReadMessages', (rid, { userId, lastSeen }) => { + ReadReceipt.markMessagesAsRead(rid, userId, lastSeen); +}); diff --git a/package-lock.json b/package-lock.json index a97aca62ca1..46ecfd847a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2825,19 +2825,23 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true + "resolved": false, + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "resolved": false, + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", - "bundled": true + "resolved": false, + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -2845,11 +2849,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": false, + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2857,57 +2863,69 @@ }, "chownr": { "version": "1.1.1", - "bundled": true + "resolved": false, + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" }, "code-point-at": { "version": "1.1.0", - "bundled": true + "resolved": false, + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "resolved": false, + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "debug": { "version": "2.6.9", - "bundled": true, + "resolved": false, + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" } }, "deep-extend": { "version": "0.6.0", - "bundled": true + "resolved": false, + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "delegates": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "detect-libc": { "version": "1.0.3", - "bundled": true + "resolved": false, + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "requires": { "minipass": "^2.2.1" } }, "fs.realpath": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": false, + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -2921,7 +2939,8 @@ }, "glob": { "version": "7.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2933,25 +2952,29 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": false, + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "requires": { "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2959,37 +2982,44 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "resolved": false, + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", - "bundled": true + "resolved": false, + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true + "resolved": false, + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.3.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2997,35 +3027,41 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "bundled": true + "resolved": false, + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "yallist": { "version": "3.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } }, "minizlib": { "version": "1.1.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "requires": { "minipass": "^2.2.1" } }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" } }, "ms": { "version": "2.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "needle": { "version": "2.2.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-GPL22d/U9cai87FcCPO6e+MT3vyHS2j+zwotakDc7kE2DtUAqFKMXLJCTtRp+5S75vXIwQPvIxkvlctxf9q4gQ==", "requires": { "debug": "^2.1.2", "iconv-lite": "^0.4.4", @@ -3034,7 +3070,8 @@ }, "node-pre-gyp": { "version": "0.11.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -3050,7 +3087,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -3058,11 +3096,13 @@ }, "npm-bundled": { "version": "1.0.5", - "bundled": true + "resolved": false, + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" }, "npm-packlist": { "version": "1.1.11", - "bundled": true, + "resolved": false, + "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" @@ -3070,7 +3110,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -3080,30 +3121,36 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", - "bundled": true + "resolved": false, + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -3111,15 +3158,18 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "process-nextick-args": { "version": "2.0.0", - "bundled": true + "resolved": false, + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -3129,13 +3179,15 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } } }, "readable-stream": { "version": "2.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -3148,38 +3200,46 @@ }, "rimraf": { "version": "2.6.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "requires": { "glob": "^7.0.5" } }, "safe-buffer": { "version": "5.1.1", - "bundled": true + "resolved": false, + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", - "bundled": true + "resolved": false, + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", - "bundled": true + "resolved": false, + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "5.5.1", - "bundled": true + "resolved": false, + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, "set-blocking": { "version": "2.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "signal-exit": { "version": "3.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3188,25 +3248,29 @@ }, "string_decoder": { "version": "1.0.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", "requires": { "safe-buffer": "~5.1.0" } }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "tar": { "version": "4.4.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", "requires": { "chownr": "^1.0.1", "fs-minipass": "^1.2.5", @@ -3219,28 +3283,33 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "bundled": true + "resolved": false, + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "yallist": { "version": "3.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } }, "util-deprecate": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" } } }, @@ -7066,9 +7135,9 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", "dev": true, "optional": true, "requires": { @@ -7094,7 +7163,7 @@ "optional": true }, "are-we-there-yet": { - "version": "1.1.4", + "version": "1.1.5", "bundled": true, "dev": true, "optional": true, @@ -7118,7 +7187,7 @@ } }, "chownr": { - "version": "1.0.1", + "version": "1.1.1", "bundled": true, "dev": true, "optional": true @@ -7154,7 +7223,7 @@ } }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true, "dev": true, "optional": true @@ -7203,7 +7272,7 @@ } }, "glob": { - "version": "7.1.2", + "version": "7.1.3", "bundled": true, "dev": true, "optional": true, @@ -7223,12 +7292,12 @@ "optional": true }, "iconv-lite": { - "version": "0.4.21", + "version": "0.4.24", "bundled": true, "dev": true, "optional": true, "requires": { - "safer-buffer": "^2.1.0" + "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { @@ -7289,16 +7358,16 @@ "dev": true }, "minipass": { - "version": "2.2.4", + "version": "2.3.5", "bundled": true, "dev": true, "requires": { - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.0" } }, "minizlib": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "dev": true, "optional": true, @@ -7321,7 +7390,7 @@ "optional": true }, "needle": { - "version": "2.2.0", + "version": "2.2.4", "bundled": true, "dev": true, "optional": true, @@ -7332,18 +7401,18 @@ } }, "node-pre-gyp": { - "version": "0.10.0", + "version": "0.10.3", "bundled": true, "dev": true, "optional": true, "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", - "needle": "^2.2.0", + "needle": "^2.2.1", "nopt": "^4.0.1", "npm-packlist": "^1.1.6", "npmlog": "^4.0.2", - "rc": "^1.1.7", + "rc": "^1.2.7", "rimraf": "^2.6.1", "semver": "^5.3.0", "tar": "^4" @@ -7360,13 +7429,13 @@ } }, "npm-bundled": { - "version": "1.0.3", + "version": "1.0.5", "bundled": true, "dev": true, "optional": true }, "npm-packlist": { - "version": "1.1.10", + "version": "1.2.0", "bundled": true, "dev": true, "optional": true, @@ -7441,12 +7510,12 @@ "optional": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "dev": true, "optional": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" @@ -7476,16 +7545,16 @@ } }, "rimraf": { - "version": "2.6.2", + "version": "2.6.3", "bundled": true, "dev": true, "optional": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "safe-buffer": { - "version": "5.1.1", + "version": "5.1.2", "bundled": true, "dev": true }, @@ -7502,7 +7571,7 @@ "optional": true }, "semver": { - "version": "5.5.0", + "version": "5.6.0", "bundled": true, "dev": true, "optional": true @@ -7553,17 +7622,17 @@ "optional": true }, "tar": { - "version": "4.4.1", + "version": "4.4.8", "bundled": true, "dev": true, "optional": true, "requires": { - "chownr": "^1.0.1", + "chownr": "^1.1.1", "fs-minipass": "^1.2.5", - "minipass": "^2.2.4", - "minizlib": "^1.1.0", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.1", + "safe-buffer": "^5.1.2", "yallist": "^3.0.2" } }, @@ -7574,12 +7643,12 @@ "optional": true }, "wide-align": { - "version": "1.1.2", + "version": "1.1.3", "bundled": true, "dev": true, "optional": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrappy": { @@ -7588,7 +7657,7 @@ "dev": true }, "yallist": { - "version": "3.0.2", + "version": "3.0.3", "bundled": true, "dev": true } @@ -7671,7 +7740,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, "get-value": { @@ -8198,19 +8267,23 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "bundled": true + "resolved": false, + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "ansi-regex": { "version": "2.1.1", - "bundled": true + "resolved": false, + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", - "bundled": true + "resolved": false, + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -8218,11 +8291,13 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", - "bundled": true, + "resolved": false, + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8230,57 +8305,69 @@ }, "chownr": { "version": "1.1.1", - "bundled": true + "resolved": false, + "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==" }, "code-point-at": { "version": "1.1.0", - "bundled": true + "resolved": false, + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", - "bundled": true + "resolved": false, + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "debug": { "version": "2.6.9", - "bundled": true, + "resolved": false, + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" } }, "deep-extend": { "version": "0.6.0", - "bundled": true + "resolved": false, + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "delegates": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "detect-libc": { "version": "1.0.3", - "bundled": true + "resolved": false, + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "fs-minipass": { "version": "1.2.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "requires": { "minipass": "^2.2.1" } }, "fs.realpath": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "gauge": { "version": "2.7.4", - "bundled": true, + "resolved": false, + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -8294,7 +8381,8 @@ }, "glob": { "version": "7.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8306,25 +8394,29 @@ }, "has-unicode": { "version": "2.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "iconv-lite": { "version": "0.4.24", - "bundled": true, + "resolved": false, + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "requires": { "safer-buffer": ">= 2.1.2 < 3" } }, "ignore-walk": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "requires": { "minimatch": "^3.0.4" } }, "inflight": { "version": "1.0.6", - "bundled": true, + "resolved": false, + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -8332,37 +8424,44 @@ }, "inherits": { "version": "2.0.3", - "bundled": true + "resolved": false, + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", - "bundled": true + "resolved": false, + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" } }, "isarray": { "version": "1.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "minimatch": { "version": "3.0.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "1.2.0", - "bundled": true + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" }, "minipass": { "version": "2.3.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -8370,31 +8469,36 @@ }, "minizlib": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg==", "requires": { "minipass": "^2.2.1" } }, "mkdirp": { "version": "0.5.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" }, "dependencies": { "minimist": { "version": "0.0.8", - "bundled": true + "resolved": false, + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" } } }, "ms": { "version": "2.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "needle": { "version": "2.2.4", - "bundled": true, + "resolved": false, + "integrity": "sha512-HyoqEb4wr/rsoaIDfTH2aVL9nWtQqba2/HvMv+++m8u0dz808MaagKILxtfeSN7QU7nvbQ79zk3vYOJp9zsNEA==", "requires": { "debug": "^2.1.2", "iconv-lite": "^0.4.4", @@ -8403,7 +8507,8 @@ }, "node-pre-gyp": { "version": "0.12.0", - "bundled": true, + "resolved": false, + "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", "requires": { "detect-libc": "^1.0.2", "mkdirp": "^0.5.1", @@ -8419,7 +8524,8 @@ }, "nopt": { "version": "4.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -8427,11 +8533,13 @@ }, "npm-bundled": { "version": "1.0.5", - "bundled": true + "resolved": false, + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" }, "npm-packlist": { "version": "1.1.12", - "bundled": true, + "resolved": false, + "integrity": "sha512-WJKFOVMeAlsU/pjXuqVdzU0WfgtIBCupkEVwn+1Y0ERAbUfWw8R4GjgVbaKnUjRoD2FoQbHOCbOyT5Mbs9Lw4g==", "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" @@ -8439,7 +8547,8 @@ }, "npmlog": { "version": "4.1.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -8449,30 +8558,36 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", - "bundled": true + "resolved": false, + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "once": { "version": "1.4.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" } }, "os-homedir": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-tmpdir": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", - "bundled": true, + "resolved": false, + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -8480,11 +8595,13 @@ }, "path-is-absolute": { "version": "1.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "process-nextick-args": { "version": "2.0.0", - "bundled": true + "resolved": false, + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "protobufjs": { "version": "5.0.3", @@ -8499,7 +8616,8 @@ }, "rc": { "version": "1.2.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -8509,7 +8627,8 @@ }, "readable-stream": { "version": "2.3.6", - "bundled": true, + "resolved": false, + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8522,38 +8641,46 @@ }, "rimraf": { "version": "2.6.2", - "bundled": true, + "resolved": false, + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "requires": { "glob": "^7.0.5" } }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "resolved": false, + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", - "bundled": true + "resolved": false, + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", - "bundled": true + "resolved": false, + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { "version": "5.6.0", - "bundled": true + "resolved": false, + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "set-blocking": { "version": "2.0.0", - "bundled": true + "resolved": false, + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "signal-exit": { "version": "3.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "string-width": { "version": "1.0.2", - "bundled": true, + "resolved": false, + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8562,25 +8689,29 @@ }, "string_decoder": { "version": "1.1.1", - "bundled": true, + "resolved": false, + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { "safe-buffer": "~5.1.0" } }, "strip-ansi": { "version": "3.0.1", - "bundled": true, + "resolved": false, + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" } }, "strip-json-comments": { "version": "2.0.1", - "bundled": true + "resolved": false, + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "tar": { "version": "4.4.8", - "bundled": true, + "resolved": false, + "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -8593,22 +8724,26 @@ }, "util-deprecate": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "wide-align": { "version": "1.1.3", - "bundled": true, + "resolved": false, + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "requires": { "string-width": "^1.0.2 || 2" } }, "wrappy": { "version": "1.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.2", - "bundled": true + "resolved": false, + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } }, @@ -8810,9 +8945,9 @@ "dev": true }, "moment": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", - "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==", + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.10.3.tgz", + "integrity": "sha1-CruZ8wf2UhgwjGk17+KcV7Ggon8=", "dev": true } } @@ -11635,7 +11770,7 @@ "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" } @@ -12207,6 +12342,14 @@ "semver": "^5.3.0" } }, + "node-rsa": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.0.3.tgz", + "integrity": "sha512-gQowjnOunjmojrpO+d8x1ubL9X2Zpj4MRmY2J2hPtVF8g1VgOX1yNWUeCCoyzkRHunJf1/3orLzit5PiRtDz1A==", + "requires": { + "asn1": "^0.2.4" + } + }, "nodejieba": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/nodejieba/-/nodejieba-2.3.0.tgz", @@ -12279,7 +12422,7 @@ "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha1-CKfyqL9zRgR3mp76StXMcXq7lUs=", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -14012,7 +14155,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true } @@ -15255,6 +15398,11 @@ "readable-stream": "^2.0.2" } }, + "stream-buffers": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", + "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==" + }, "stream-each": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", @@ -16757,535 +16905,6 @@ "readdirp": "^2.2.1", "upath": "^1.1.0" } - }, - "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true - } - } } } }, diff --git a/package.json b/package.json index 94d2ad66b8a..7c3b9949d26 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,7 @@ "moment": "^2.22.2", "moment-timezone": "^0.5.23", "node-dogstatsd": "^0.0.7", + "node-rsa": "^1.0.3", "object-path": "^0.11.4", "pdfjs-dist": "^2.0.943", "photoswipe": "^4.1.2", @@ -205,6 +206,7 @@ "semver": "^5.6.0", "sharp": "^0.21.0", "speakeasy": "^2.0.0", + "stream-buffers": "^3.0.2", "subscriptions-transport-ws": "^0.9.11", "tar-stream": "^1.6.2", "toastr": "^2.1.4", @@ -214,6 +216,7 @@ "ua-parser-js": "^0.7.19", "underscore": "^1.9.1", "underscore.string": "^3.3.5", + "uuid": "^3.3.2", "webdav": "^2.0.0", "wolfy87-eventemitter": "^5.2.5", "xml-crypto": "^1.0.2", diff --git a/packages/rocketchat-api/server/v1/misc.js b/packages/rocketchat-api/server/v1/misc.js index 7528fe4f0bb..bb8d8820bc3 100644 --- a/packages/rocketchat-api/server/v1/misc.js +++ b/packages/rocketchat-api/server/v1/misc.js @@ -152,7 +152,7 @@ API.v1.addRoute('directory', { authRequired: true }, { const { offset, count } = this.getPaginationItems(); const { sort, query } = this.parseJsonQuery(); - const { text, type } = query; + const { text, type, workspace = 'local' } = query; if (sort && Object.keys(sort).length > 1) { return API.v1.failure('This method support only one "sort" parameter'); } @@ -162,6 +162,7 @@ API.v1.addRoute('directory', { authRequired: true }, { const result = Meteor.runAsUser(this.userId, () => Meteor.call('browseChannels', { text, type, + workspace, sortBy, sortDirection, offset: Math.max(0, offset), diff --git a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js index 4477411577d..462a252cee0 100644 --- a/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js +++ b/packages/rocketchat-channel-settings/server/methods/saveRoomSettings.js @@ -2,6 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasPermission } from 'meteor/rocketchat:authorization'; import { Rooms } from 'meteor/rocketchat:models'; +import { callbacks } from 'meteor/rocketchat:callbacks'; + import { saveRoomName } from '../functions/saveRoomName'; import { saveRoomTopic } from '../functions/saveRoomTopic'; import { saveRoomAnnouncement } from '../functions/saveRoomAnnouncement'; @@ -212,6 +214,11 @@ Meteor.methods({ } }); + Meteor.defer(function() { + const room = Rooms.findOneById(rid); + callbacks.run('afterSaveRoomSettings', room); + }); + return { result: true, rid: room._id, diff --git a/packages/rocketchat-federation/README.md b/packages/rocketchat-federation/README.md new file mode 100644 index 00000000000..e8f08c10233 --- /dev/null +++ b/packages/rocketchat-federation/README.md @@ -0,0 +1 @@ +##Rocket.Chat Federation \ No newline at end of file diff --git a/packages/rocketchat-federation/client/main.js b/packages/rocketchat-federation/client/main.js new file mode 100644 index 00000000000..b93db97bfb7 --- /dev/null +++ b/packages/rocketchat-federation/client/main.js @@ -0,0 +1,23 @@ +import { MessageTypes } from 'meteor/rocketchat:ui-utils'; + +// Register message types +MessageTypes.registerType({ + id: 'rejected-message-by-peer', + system: true, + message: 'This_message_was_rejected_by__peer__peer', + data(message) { + return { + peer: message.peer, + }; + }, +}); +MessageTypes.registerType({ + id: 'peer-does-not-exist', + system: true, + message: 'The_peer__peer__does_not_exist', + data(message) { + return { + peer: message.peer, + }; + }, +}); diff --git a/packages/rocketchat-federation/package.js b/packages/rocketchat-federation/package.js new file mode 100644 index 00000000000..2876da93d30 --- /dev/null +++ b/packages/rocketchat-federation/package.js @@ -0,0 +1,23 @@ +Package.describe({ + name: 'rocketchat:federation', + version: '0.0.1', + summary: 'RocketChat support for federating with other RocketChat servers', + git: '', +}); + +Package.onUse(function(api) { + api.use([ + 'ecmascript', + 'rocketchat:api', + 'rocketchat:lib', + 'rocketchat:reactions', + 'rocketchat:models', + 'rocketchat:settings', + ]); + + api.use('accounts-base', 'server'); + api.use('accounts-password', 'server'); + + api.mainModule('client/main.js', 'client'); + api.mainModule('server/main.js', 'server'); +}); diff --git a/packages/rocketchat-federation/server/federatedResources/FederatedMessage.js b/packages/rocketchat-federation/server/federatedResources/FederatedMessage.js new file mode 100644 index 00000000000..68c4099358c --- /dev/null +++ b/packages/rocketchat-federation/server/federatedResources/FederatedMessage.js @@ -0,0 +1,264 @@ +import { Meteor } from 'meteor/meteor'; +import { sendMessage, updateMessage } from 'meteor/rocketchat:lib'; +import { Messages, Rooms, Users } from 'meteor/rocketchat:models'; +import { FileUpload } from 'meteor/rocketchat:file-upload'; + +import FederatedResource from './FederatedResource'; +import FederatedRoom from './FederatedRoom'; +import FederatedUser from './FederatedUser'; +import peerClient from '../peerClient'; + +class FederatedMessage extends FederatedResource { + constructor(localPeerIdentifier, message) { + super('message'); + + if (!message) { + throw new Error('message param cannot be empty'); + } + + this.localPeerIdentifier = localPeerIdentifier; + + // Make sure room dates are correct + message.ts = new Date(message.ts); + message._updatedAt = new Date(message._updatedAt); + + // Set the message author + if (message.u.federation) { + this.federatedAuthor = FederatedUser.loadByFederationId(localPeerIdentifier, message.u.federation._id); + } else { + const author = Users.findOneById(message.u._id); + this.federatedAuthor = new FederatedUser(localPeerIdentifier, author); + } + + message.u = { + username: this.federatedAuthor.user.username, + federation: { + _id: this.federatedAuthor.user.federation._id, + }, + }; + + // Set the room + const room = Rooms.findOneById(message.rid); + + // Prepare the federation property + if (!message.federation) { + const federation = { + _id: message._id, + peer: localPeerIdentifier, + roomId: room.federation._id, + }; + + // Prepare the user + message.federation = federation; + + // Update the user + Messages.update(message._id, { $set: { federation } }); + + // Prepare mentions + for (const mention of message.mentions) { + + mention.federation = mention.federation || {}; + + if (mention.username.indexOf('@') === -1) { + mention.federation.peer = localPeerIdentifier; + } else { + const [username, peer] = mention.username.split('@'); + + mention.username = username; + mention.federation.peer = peer; + } + } + + // Prepare channels + for (const channel of message.channels) { + channel.federation = channel.federation || {}; + + if (channel.name.indexOf('@') === -1) { + channel.federation.peer = localPeerIdentifier; + } else { + channel.name = channel.name.split('@')[0]; + channel.federation.peer = channel.name.split('@')[1]; + } + } + } + + // Set message property + this.message = message; + } + + getFederationId() { + return this.message.federation._id; + } + + getMessage() { + return this.message; + } + + getLocalMessage() { + this.log('getLocalMessage'); + + const { localPeerIdentifier, message } = this; + + const localMessage = Object.assign({}, message); + + // Make sure `u` is correct + if (!this.federatedAuthor) { + throw new Error('Author does not exist'); + } + + const localAuthor = this.federatedAuthor.getLocalUser(); + + localMessage.u = { + _id: localAuthor._id, + username: localAuthor.username, + }; + + // Make sure `rid` is correct + const federatedRoom = FederatedRoom.loadByFederationId(localPeerIdentifier, message.federation.roomId); + + if (!federatedRoom) { + throw new Error('Room does not exist'); + } + + const localRoom = federatedRoom.getLocalRoom(); + + localMessage.rid = localRoom._id; + + return localMessage; + } + + create() { + this.log('create'); + + // Get the local message object + const localMessageObject = this.getLocalMessage(); + + // Grab the federation id + const { federation: { _id: federationId } } = localMessageObject; + + // Check if the message exists + let localMessage = Messages.findOne({ 'federation._id': federationId }); + + // Create if needed + if (!localMessage) { + delete localMessageObject._id; + + localMessage = localMessageObject; + + const localRoom = Rooms.findOneById(localMessage.rid); + + // Normalize mentions + for (const mention of localMessage.mentions) { + // Ignore if we are dealing with all, here or rocket.cat + if (['all', 'here', 'rocket.cat'].indexOf(mention.username) !== -1) { continue; } + + let usernameToReplace = ''; + + if (mention.federation.peer !== this.localPeerIdentifier) { + usernameToReplace = mention.username; + + mention.username = `${ mention.username }@${ mention.federation.peer }`; + } else { + usernameToReplace = `${ mention.username }@${ mention.federation.peer }`; + } + + localMessage.msg = localMessage.msg.split(usernameToReplace).join(mention.username); + } + + // Normalize channels + for (const channel of localMessage.channels) { + if (channel.federation.peer !== this.localPeerIdentifier) { + channel.name = `${ channel.name }@${ channel.federation.peer }`; + } + } + + // Is there a file? + if (localMessage.file) { + const fileStore = FileUpload.getStore('Uploads'); + + const { federation: { peer: identifier } } = localMessage; + + const { upload, buffer } = peerClient.getUpload({ identifier, localMessage }); + + const oldUploadId = upload._id; + + // Normalize upload + delete upload._id; + upload.rid = localMessage.rid; + upload.userId = localMessage.u._id; + upload.federation = { + _id: localMessage.file._id, + peer: identifier, + }; + + Meteor.runAsUser(upload.userId, () => Meteor.wrapAsync(fileStore.insert.bind(fileStore))(upload, buffer)); + + // Update the message's file + localMessage.file._id = upload._id; + + // Update the message's attachments + for (const attachment of localMessage.attachments) { + attachment.title_link = attachment.title_link.replace(oldUploadId, upload._id); + attachment.image_url = attachment.image_url.replace(oldUploadId, upload._id); + } + } + + // Create the message + const { _id } = sendMessage(localMessage.u, localMessage, localRoom, false); + + localMessage._id = _id; + } + + return localMessage; + } + + update(updatedByFederatedUser) { + this.log('update'); + + // Get the original message + const originalMessage = Messages.findOne({ 'federation._id': this.getFederationId() }); + + // Error if message does not exist + if (!originalMessage) { + throw new Error('Message does not exist'); + } + + // Get the local message object + const localMessage = this.getLocalMessage(); + + // Make sure the message has the correct _id + localMessage._id = originalMessage._id; + + // Get the user who updated + const user = updatedByFederatedUser.getLocalUser(); + + // Update the message + updateMessage(localMessage, user, originalMessage); + + return localMessage; + } +} + +FederatedMessage.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) { + const localMessage = Messages.findOne({ 'federation._id': federationId }); + + if (!localMessage) { return; } + + return new FederatedMessage(localPeerIdentifier, localMessage); +}; + +FederatedMessage.loadOrCreate = function loadOrCreate(localPeerIdentifier, message) { + const { federation } = message; + + if (federation) { + const federatedMessage = FederatedMessage.loadByFederationId(localPeerIdentifier, federation._id); + + if (federatedMessage) { + return federatedMessage; + } + } + + return new FederatedMessage(localPeerIdentifier, message); +}; + +export default FederatedMessage; diff --git a/packages/rocketchat-federation/server/federatedResources/FederatedResource.js b/packages/rocketchat-federation/server/federatedResources/FederatedResource.js new file mode 100644 index 00000000000..772dc30348b --- /dev/null +++ b/packages/rocketchat-federation/server/federatedResources/FederatedResource.js @@ -0,0 +1,19 @@ +import { logger } from '../logger'; + +class FederatedResource { + constructor(name) { + this.resourceName = `federated-${ name }`; + + this.log('Creating federated resource'); + } + + log(message) { + FederatedResource.log(this.resourceName, message); + } +} + +FederatedResource.log = function log(name, message) { + logger.resource.info(`[${ name }] ${ message }`); +}; + +export default FederatedResource; diff --git a/packages/rocketchat-federation/server/federatedResources/FederatedRoom.js b/packages/rocketchat-federation/server/federatedResources/FederatedRoom.js new file mode 100644 index 00000000000..c546f0941e0 --- /dev/null +++ b/packages/rocketchat-federation/server/federatedResources/FederatedRoom.js @@ -0,0 +1,270 @@ +import { createRoom } from 'meteor/rocketchat:lib'; +import { Rooms, Subscriptions, Users } from 'meteor/rocketchat:models'; + +import FederatedResource from './FederatedResource'; +import FederatedUser from './FederatedUser'; + +class FederatedRoom extends FederatedResource { + constructor(localPeerIdentifier, room, extras = {}) { + super('room'); + + if (!room) { + throw new Error('room param cannot be empty'); + } + + this.localPeerIdentifier = localPeerIdentifier; + + // Make sure room dates are correct + room.ts = new Date(room.ts); + room._updatedAt = new Date(room._updatedAt); + + // Set the name + if (room.t !== 'd' && room.name.indexOf('@') === -1) { + room.name = `${ room.name }@${ localPeerIdentifier }`; + } + + // Set the federated owner, if there is one + const { owner } = extras; + + if (owner) { + if (!owner && room.federation) { + this.federatedOwner = FederatedUser.loadByFederationId(localPeerIdentifier, room.federation.ownerId); + } else { + this.federatedOwner = FederatedUser.loadOrCreate(localPeerIdentifier, owner); + } + } + + // Set base federation + room.federation = room.federation || { + _id: room._id, + peer: localPeerIdentifier, + ownerId: this.federatedOwner ? this.federatedOwner.getFederationId() : null, + }; + + // Set room property + this.room = room; + } + + getFederationId() { + return this.room.federation._id; + } + + getPeers() { + return this.room.federation.peers; + } + + getRoom() { + return this.room; + } + + getOwner() { + return this.federatedOwner ? this.federatedOwner.getUser() : null; + } + + getUsers() { + return this.federatedUsers.map((u) => u.getUser()); + } + + loadUsers() { + const { room } = this; + + // Get all room users + const users = FederatedRoom.loadRoomUsers(room); + + this.setUsers(users); + } + + setUsers(users) { + const { localPeerIdentifier } = this; + + // Initialize federatedUsers + this.federatedUsers = []; + + for (const user of users) { + const federatedUser = FederatedUser.loadOrCreate(localPeerIdentifier, user); + + // Keep the federated user + this.federatedUsers.push(federatedUser); + } + } + + refreshFederation() { + const { room } = this; + + // Prepare the federated users + let federation = { + peers: [], + users: [], + }; + + // Check all the peers + for (const federatedUser of this.federatedUsers) { + // Add federation data to the room + const { user: { federation: { _id, peer } } } = federatedUser; + + federation.peers.push(peer); + federation.users.push({ _id, peer }); + } + + federation.peers = [...new Set(federation.peers)]; + + federation = Object.assign(room.federation || {}, federation); + + // Prepare the room + room.federation = federation; + + // Update the room + Rooms.update(room._id, { $set: { federation } }); + } + + getLocalRoom() { + this.log('getLocalRoom'); + + const { localPeerIdentifier, room, room: { federation } } = this; + + const localRoom = Object.assign({}, room); + + if (federation.peer === localPeerIdentifier) { + if (localRoom.t !== 'd') { + localRoom.name = room.name.split('@')[0]; + } + } + + return localRoom; + } + + createUsers() { + this.log('createUsers'); + + const { federatedUsers } = this; + + // Create, if needed, all room's users + for (const federatedUser of federatedUsers) { + federatedUser.create(); + } + } + + create() { + this.log('create'); + + // Get the local room object (with or without suffixes) + const localRoomObject = this.getLocalRoom(); + + // Grab the federation id + const { federation: { _id: federationId } } = localRoomObject; + + // Check if the user exists + let localRoom = FederatedRoom.loadByFederationId(this.localPeerIdentifier, federationId); + + // Create if needed + if (!localRoom) { + delete localRoomObject._id; + + localRoom = localRoomObject; + + const { t: type, name, broadcast, customFields, federation, sysMes } = localRoom; + const { federatedOwner, federatedUsers } = this; + + // Get usernames for the owner and members + const ownerUsername = federatedOwner.user.username; + const members = []; + + if (type !== 'd') { + for (const federatedUser of federatedUsers) { + const localUser = federatedUser.getLocalUser(); + members.push(localUser.username); + } + } else { + for (const federatedUser of federatedUsers) { + const localUser = federatedUser.getLocalUser(); + members.push(localUser); + } + } + + // Is this a broadcast channel? Then mute everyone but the owner + let muted = []; + + if (broadcast) { + muted = members.filter((u) => u !== ownerUsername); + } + + // Set the extra data and create room options + let extraData = { + federation, + }; + + let createRoomOptions = { + subscriptionExtra: { + alert: true, + open: true, + }, + }; + + if (type !== 'd') { + extraData = Object.assign(extraData, { + broadcast, + customFields, + encrypted: false, // Always false for now + muted, + sysMes, + }); + + createRoomOptions = Object.assign(extraData, { + nameValidationRegex: '^[0-9a-zA-Z-_.@]+$', + subscriptionExtra: { + alert: true, + }, + }); + } + + // Create the room + // !!!! Forcing direct or private only, no public rooms for now + const { rid } = createRoom(type === 'd' ? type : 'p', name, ownerUsername, members, false, extraData, createRoomOptions); + + localRoom._id = rid; + } + + return localRoom; + } +} + +FederatedRoom.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) { + const localRoom = Rooms.findOne({ 'federation._id': federationId }); + + if (!localRoom) { return; } + + return new FederatedRoom(localPeerIdentifier, localRoom); +}; + +FederatedRoom.loadRoomUsers = function loadRoomUsers(room) { + const subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id, { fields: { 'u._id': 1 } }).fetch(); + const userIds = subscriptions.map((s) => s.u._id); + return Users.findUsersWithUsernameByIds(userIds).fetch(); +}; + +FederatedRoom.isFederated = function isFederated(localPeerIdentifier, room, options = {}) { + this.log('federated-room', `${ room._id } - isFederated?`); + + let isFederated = false; + + if (options.checkUsingUsers) { + // Get all room users + const users = FederatedRoom.loadRoomUsers(room); + + // Check all the users + for (const user of users) { + if (user.federation && user.federation.peer !== localPeerIdentifier) { + isFederated = true; + break; + } + } + } else { + isFederated = room.federation && room.federation.peers.length > 1; + } + + this.log('federated-room', `${ room._id } - isFederated? ${ isFederated ? 'yes' : 'no' }`); + + return isFederated; +}; + +export default FederatedRoom; diff --git a/packages/rocketchat-federation/server/federatedResources/FederatedUser.js b/packages/rocketchat-federation/server/federatedResources/FederatedUser.js new file mode 100644 index 00000000000..d5d5b7c03a1 --- /dev/null +++ b/packages/rocketchat-federation/server/federatedResources/FederatedUser.js @@ -0,0 +1,124 @@ +import { Users } from 'meteor/rocketchat:models'; + +import FederatedResource from './FederatedResource'; + +class FederatedUser extends FederatedResource { + constructor(localPeerIdentifier, user) { + super('user'); + + if (!user) { + throw new Error('user param cannot be empty'); + } + + this.localPeerIdentifier = localPeerIdentifier; + + // Make sure all properties are normalized + // Prepare the federation property + if (!user.federation) { + const federation = { + _id: user._id, + peer: localPeerIdentifier, + }; + + // Prepare the user + user.federation = federation; + + // Update the user + Users.update(user._id, { $set: { federation } }); + } + + // Make sure user dates are correct + user.createdAt = new Date(user.createdAt); + user.lastLogin = new Date(user.lastLogin); + user._updatedAt = new Date(user._updatedAt); + + // Delete sensitive data as well + delete user.roles; + delete user.services; + + // Make sure some other properties are ready + user.name = user.name; + user.username = user.username.indexOf('@') === -1 ? `${ user.username }@${ user.federation.peer }` : user.username; + user.roles = ['user']; + user.status = 'online'; + user.statusConnection = 'online'; + user.type = 'user'; + + // Set user property + this.user = user; + } + + getFederationId() { + return this.user.federation._id; + } + + getUser() { + return this.user; + } + + getLocalUser() { + this.log('getLocalUser'); + + const { localPeerIdentifier, user, user: { federation } } = this; + + const localUser = Object.assign({}, user); + + if (federation.peer === localPeerIdentifier || user.username === 'rocket.cat') { + localUser.username = user.username.split('@')[0]; + localUser.name = user.name.split('@')[0]; + } + + return localUser; + } + + create() { + this.log('create'); + + // Get the local user object (with or without suffixes) + const localUserObject = this.getLocalUser(); + + // Grab the federation id + const { federation: { _id: federationId } } = localUserObject; + + // Check if the user exists + let localUser = Users.findOne({ 'federation._id': federationId }); + + // Create if needed + if (!localUser) { + delete localUserObject._id; + + localUser = localUserObject; + + localUser._id = Users.create(localUserObject); + } + + // Update the id + this.user._id = localUser._id; + + return localUser; + } +} + +FederatedUser.loadByFederationId = function loadByFederationId(localPeerIdentifier, federationId) { + const localUser = Users.findOne({ 'federation._id': federationId }); + + if (!localUser) { return; } + + return new FederatedUser(localPeerIdentifier, localUser); +}; + +FederatedUser.loadOrCreate = function loadOrCreate(localPeerIdentifier, user) { + const { federation } = user; + + if (federation) { + const federatedUser = FederatedUser.loadByFederationId(localPeerIdentifier, federation._id); + + if (federatedUser) { + return federatedUser; + } + } + + return new FederatedUser(localPeerIdentifier, user); +}; + +export default FederatedUser; diff --git a/packages/rocketchat-federation/server/federatedResources/index.js b/packages/rocketchat-federation/server/federatedResources/index.js new file mode 100644 index 00000000000..0bd6fdd4cbb --- /dev/null +++ b/packages/rocketchat-federation/server/federatedResources/index.js @@ -0,0 +1,4 @@ +export { default as FederatedMessage } from './FederatedMessage'; +export { default as FederatedResource } from './FederatedResource'; +export { default as FederatedRoom } from './FederatedRoom'; +export { default as FederatedUser } from './FederatedUser'; diff --git a/packages/rocketchat-federation/server/federation-settings.js b/packages/rocketchat-federation/server/federation-settings.js new file mode 100644 index 00000000000..82e082747f5 --- /dev/null +++ b/packages/rocketchat-federation/server/federation-settings.js @@ -0,0 +1,68 @@ +import { Meteor } from 'meteor/meteor'; +import { settings } from 'meteor/rocketchat:settings'; +import { FederationKeys } from 'meteor/rocketchat:models'; + +Meteor.startup(function() { + // const federationUniqueId = FederationKeys.getUniqueId(); + const federationPublicKey = FederationKeys.getPublicKeyString(); + + settings.addGroup('Federation', function() { + this.add('FEDERATION_Enabled', false, { + type: 'boolean', + i18nLabel: 'Enabled', + i18nDescription: 'FEDERATION_Enabled', + alert: 'FEDERATION_Enabled_Alert', + public: true, + }); + + this.add('FEDERATION_Status', '-', { + readonly: true, + type: 'string', + i18nLabel: 'FEDERATION_Status', + }); + + // this.add('FEDERATION_Unique_Id', federationUniqueId, { + // readonly: true, + // type: 'string', + // i18nLabel: 'FEDERATION_Unique_Id', + // i18nDescription: 'FEDERATION_Unique_Id_Description', + // }); + + this.add('FEDERATION_Domain', '', { + type: 'string', + i18nLabel: 'FEDERATION_Domain', + i18nDescription: 'FEDERATION_Domain_Description', + alert: 'FEDERATION_Domain_Alert', + }); + + this.add('FEDERATION_Public_Key', federationPublicKey, { + readonly: true, + type: 'string', + multiline: true, + i18nLabel: 'FEDERATION_Public_Key', + i18nDescription: 'FEDERATION_Public_Key_Description', + }); + + this.add('FEDERATION_Hub_URL', 'https://hub.rocket.chat', { + group: 'Federation Hub', + type: 'string', + i18nLabel: 'FEDERATION_Hub_URL', + i18nDescription: 'FEDERATION_Hub_URL_Description', + }); + + this.add('FEDERATION_Discovery_Method', 'dns', { + type: 'select', + values: [{ + key: 'dns', + i18nLabel: 'DNS', + }, { + key: 'hub', + i18nLabel: 'Hub', + }], + i18nLabel: 'FEDERATION_Discovery_Method', + i18nDescription: 'FEDERATION_Discovery_Method_Description', + public: true, + }); + + }); +}); diff --git a/packages/rocketchat-federation/server/logger.js b/packages/rocketchat-federation/server/logger.js new file mode 100644 index 00000000000..bad916731ab --- /dev/null +++ b/packages/rocketchat-federation/server/logger.js @@ -0,0 +1,12 @@ +import { Logger } from 'meteor/rocketchat:logger'; + +export const logger = new Logger('Federation', { + sections: { + resource: 'Resource', + setup: 'Setup', + peerClient: 'Peer Client', + peerServer: 'Peer Server', + dns: 'DNS', + http: 'HTTP', + }, +}); diff --git a/packages/rocketchat-federation/server/main.js b/packages/rocketchat-federation/server/main.js new file mode 100644 index 00000000000..33e4c4b31e1 --- /dev/null +++ b/packages/rocketchat-federation/server/main.js @@ -0,0 +1,152 @@ +import { Meteor } from 'meteor/meteor'; +import { _ } from 'meteor/underscore'; +import { settings } from 'meteor/rocketchat:settings'; +import { FederationKeys } from 'meteor/rocketchat:models'; + +import './federation-settings'; +import './methods'; + +import { logger } from './logger'; +import peerClient from './peerClient'; +import peerServer from './peerServer'; +import peerDNS from './peerDNS'; +import peerHTTP from './peerHTTP'; +import * as SettingsUpdater from './settingsUpdater'; + +export const Federation = { + enabled: false, + privateKey: null, + publicKey: null, + usingHub: null, + uniqueId: null, + localIdentifier: null, +}; + +// Generate keys + +// Create unique id if needed +if (!FederationKeys.getUniqueId()) { + FederationKeys.generateUniqueId(); +} + +// Create key pair if needed +if (!FederationKeys.getPublicKey()) { + FederationKeys.generateKeys(); +} + +// Initializations + +// Start the client, setting up all the callbacks +peerClient.start(); + +// Start the server, setting up all the endpoints +peerServer.start(); + +const updateSettings = _.debounce(Meteor.bindEnvironment(function() { + const _enabled = settings.get('FEDERATION_Enabled'); + + if (!_enabled) { return; } + + // If it is enabled, check if the settings are there + const _uniqueId = settings.get('FEDERATION_Unique_Id'); + const _domain = settings.get('FEDERATION_Domain'); + const _discoveryMethod = settings.get('FEDERATION_Discovery_Method'); + const _hubUrl = settings.get('FEDERATION_Hub_URL'); + const _peerUrl = settings.get('Site_Url'); + + if (!_domain || !_discoveryMethod || !_hubUrl || !_peerUrl) { + SettingsUpdater.updateStatus('Could not enable, settings are not fully set'); + + logger.setup.error('Could not enable Federation, settings are not fully set'); + + return; + } + + logger.setup.info('Updating settings...'); + + // Normalize the config values + const config = { + hub: { + active: _discoveryMethod === 'hub', + url: _hubUrl.replace(/\/+$/, ''), + }, + peer: { + uniqueId: _uniqueId, + domain: _domain.replace('@', '').trim(), + url: _peerUrl.replace(/\/+$/, ''), + public_key: FederationKeys.getPublicKeyString(), + }, + }; + + // If the settings are correctly set, let's update the configuration + + // Get the key pair + Federation.privateKey = FederationKeys.getPrivateKey(); + Federation.publicKey = FederationKeys.getPublicKey(); + + // Set important information + Federation.enabled = true; + Federation.usingHub = config.hub.active; + Federation.uniqueId = config.peer.uniqueId; + Federation.localIdentifier = config.peer.domain; + + // Set DNS + peerDNS.setConfig(config); + + // Set HTTP + peerHTTP.setConfig(config); + + // Set Client + peerClient.setConfig(config); + peerClient.enable(); + + // Set server + peerServer.setConfig(config); + peerServer.enable(); + + // Register the client + if (peerClient.register()) { + SettingsUpdater.updateStatus('Running'); + } else { + SettingsUpdater.updateNextStatusTo('Disabled, could not register with Hub'); + SettingsUpdater.updateEnabled(false); + } +}), 150); + +function enableOrDisable() { + const _enabled = settings.get('FEDERATION_Enabled'); + + // If it was enabled, and was disabled now, + // make sure we disable everything: callbacks and endpoints + if (Federation.enabled && !_enabled) { + peerClient.disable(); + peerServer.disable(); + + // Disable federation + Federation.enabled = false; + + SettingsUpdater.updateStatus('Disabled'); + + logger.setup.info('Shutting down...'); + + return; + } + + // If not enabled, skip + if (!_enabled) { + SettingsUpdater.updateStatus('Disabled'); + return; + } + + logger.setup.info('Booting...'); + + SettingsUpdater.updateStatus('Booting...'); + + updateSettings(); +} + +// Add settings listeners +settings.get('FEDERATION_Enabled', enableOrDisable); +settings.get('FEDERATION_Domain', updateSettings); +settings.get('FEDERATION_Discovery_Method', updateSettings); +settings.get('FEDERATION_Hub_URL', updateSettings); diff --git a/packages/rocketchat-federation/server/methods/federationAddUser.js b/packages/rocketchat-federation/server/methods/federationAddUser.js new file mode 100644 index 00000000000..7304af7ccee --- /dev/null +++ b/packages/rocketchat-federation/server/methods/federationAddUser.js @@ -0,0 +1,48 @@ +import { Meteor } from 'meteor/meteor'; +import { Users } from 'meteor/rocketchat:models'; + +import { logger } from '../logger'; +import peerClient from '../peerClient'; +import peerServer from '../peerClient'; + +Meteor.methods({ + federationAddUser(emailAddress, domainOverride) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationAddUser' }); + } + + if (!peerServer.enabled) { + throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationAddUser' }); + } + + // Make sure the federated user still exists, and get the unique one, by email address + const [federatedUser] = peerClient.findUsers(emailAddress, { domainOverride, emailOnly: true }); + + if (!federatedUser) { + throw new Meteor.Error('federation-invalid-user', 'There is no user to add.'); + } + + let user = null; + + const localUser = federatedUser.getLocalUser(); + + localUser.name += `@${ federatedUser.user.federation.peer }`; + + // Delete the _id + delete localUser._id; + + try { + // Create the local user + user = Users.create(localUser); + } catch (err) { + // If the user already exists, return the existing user + if (err.code === 11000) { + user = Users.findOne({ 'federation._id': localUser.federation._id }); + } + + logger.error(err); + } + + return user; + }, +}); diff --git a/packages/rocketchat-federation/server/methods/federationSearchUsers.js b/packages/rocketchat-federation/server/methods/federationSearchUsers.js new file mode 100644 index 00000000000..cad994083e3 --- /dev/null +++ b/packages/rocketchat-federation/server/methods/federationSearchUsers.js @@ -0,0 +1,24 @@ +import { Meteor } from 'meteor/meteor'; + +import peerClient from '../peerClient'; +import peerServer from '../peerServer'; + +Meteor.methods({ + federationSearchUsers(email) { + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'federationSearchUsers' }); + } + + if (!peerServer.enabled) { + throw new Meteor.Error('error-federation-disabled', 'Federation disabled', { method: 'federationAddUser' }); + } + + const federatedUsers = peerClient.findUsers(email); + + if (!federatedUsers.length) { + throw new Meteor.Error('federation-user-not-found', `Could not find federated users using "${ email }"`); + } + + return federatedUsers; + }, +}); diff --git a/packages/rocketchat-federation/server/methods/index.js b/packages/rocketchat-federation/server/methods/index.js new file mode 100644 index 00000000000..ce95109beb4 --- /dev/null +++ b/packages/rocketchat-federation/server/methods/index.js @@ -0,0 +1,2 @@ +import './federationSearchUsers'; +import './federationAddUser'; diff --git a/packages/rocketchat-federation/server/peerClient.js b/packages/rocketchat-federation/server/peerClient.js new file mode 100644 index 00000000000..4681cd42975 --- /dev/null +++ b/packages/rocketchat-federation/server/peerClient.js @@ -0,0 +1,614 @@ +import qs from 'querystring'; +import { Meteor } from 'meteor/meteor'; +import { callbacks } from 'meteor/rocketchat:callbacks'; +import { settings } from 'meteor/rocketchat:settings'; +import { FederationEvents, FederationKeys, Messages, Rooms, Subscriptions, Users } from 'meteor/rocketchat:models'; + +import { Federation } from './main'; +import peerDNS from './peerDNS'; +import peerHTTP from './peerHTTP'; +import { updateStatus } from './settingsUpdater'; +import { logger } from './logger'; +import { FederatedMessage, FederatedRoom, FederatedUser } from './federatedResources'; + +class PeerClient { + constructor() { + this.config = {}; + + this.enabled = false; + + // Keep resources we should skip callbacks + this.callbacksToSkip = {}; + } + + setConfig(config) { + // General + this.config = config; + + // Setup HubPeer + const { hub: { url } } = this.config; + + // Remove trailing slash + this.HubPeer = { url }; + + // Set the local peer + this.peer = { + domain: this.config.peer.domain, + url: this.config.peer.url, + public_key: this.config.peer.public_key, + }; + } + + log(message) { + logger.peerClient.info(message); + } + + disable() { + this.log('Disabling...'); + + this.enabled = false; + } + + enable() { + this.log('Enabling...'); + + this.enabled = true; + } + + start() { + this.setupCallbacks(); + } + + // ########### + // + // Registering + // + // ########### + register() { + if (this.config.hub.active) { + updateStatus('Registering with Hub...'); + + return peerDNS.register(this.peer); + } + + return true; + } + + // ################### + // + // Callback management + // + // ################### + addCallbackToSkip(callback, resourceId) { + this.callbacksToSkip[`${ callback }_${ resourceId }`] = true; + } + + skipCallbackIfNeeded(callback, resource) { + const { federation } = resource; + + if (!federation) { return false; } + + const { _id } = federation; + + const callbackName = `${ callback }_${ _id }`; + + const skipCallback = this.callbacksToSkip[callbackName]; + + delete this.callbacksToSkip[callbackName]; + + this.log(`${ callbackName } callback ${ skipCallback ? '' : 'not ' }skipped`); + + return skipCallback; + } + + wrapEnabled(callbackHandler) { + return function(...parameters) { + if (!this.enabled) { return; } + + callbackHandler.apply(this, parameters); + }.bind(this); + } + + setupCallbacks() { + // Accounts.onLogin(onLoginCallbackHandler.bind(this)); + // Accounts.onLogout(onLogoutCallbackHandler.bind(this)); + + FederationEvents.on('createEvent', this.wrapEnabled(this.onCreateEvent.bind(this))); + + callbacks.add('afterCreateDirectRoom', this.wrapEnabled(this.afterCreateDirectRoom.bind(this)), callbacks.priority.LOW, 'federation-create-direct-room'); + callbacks.add('afterCreateRoom', this.wrapEnabled(this.afterCreateRoom.bind(this)), callbacks.priority.LOW, 'federation-join-room'); + callbacks.add('afterSaveRoomSettings', this.wrapEnabled(this.afterSaveRoomSettings.bind(this)), callbacks.priority.LOW, 'federation-after-save-room-settings'); + callbacks.add('afterAddedToRoom', this.wrapEnabled(this.afterAddedToRoom.bind(this)), callbacks.priority.LOW, 'federation-join-room'); + callbacks.add('beforeLeaveRoom', this.wrapEnabled(this.beforeLeaveRoom.bind(this)), callbacks.priority.LOW, 'federation-leave-room'); + callbacks.add('beforeRemoveFromRoom', this.wrapEnabled(this.beforeRemoveFromRoom.bind(this)), callbacks.priority.LOW, 'federation-leave-room'); + callbacks.add('afterSaveMessage', this.wrapEnabled(this.afterSaveMessage.bind(this)), callbacks.priority.LOW, 'federation-save-message'); + callbacks.add('afterDeleteMessage', this.wrapEnabled(this.afterDeleteMessage.bind(this)), callbacks.priority.LOW, 'federation-delete-message'); + callbacks.add('afterReadMessages', this.wrapEnabled(this.afterReadMessages.bind(this)), callbacks.priority.LOW, 'federation-read-messages'); + callbacks.add('afterSetReaction', this.wrapEnabled(this.afterSetReaction.bind(this)), callbacks.priority.LOW, 'federation-after-set-reaction'); + callbacks.add('afterUnsetReaction', this.wrapEnabled(this.afterUnsetReaction.bind(this)), callbacks.priority.LOW, 'federation-after-unset-reaction'); + callbacks.add('afterMuteUser', this.wrapEnabled(this.afterMuteUser.bind(this)), callbacks.priority.LOW, 'federation-mute-user'); + callbacks.add('afterUnmuteUser', this.wrapEnabled(this.afterUnmuteUser.bind(this)), callbacks.priority.LOW, 'federation-unmute-user'); + + this.log('Callbacks set'); + } + + // ################ + // + // Event management + // + // ################ + propagateEvent(e) { + this.log(`propagateEvent: ${ e.t }`); + + const { peer: domain } = e; + + const peer = peerDNS.searchPeer(domain); + + if (!peer || !peer.public_key) { + this.log(`Could not find valid peer:${ domain }`); + + FederationEvents.setEventAsErrored(e, 'Could not find valid peer'); + } else { + try { + const stringPayload = JSON.stringify({ event: e }); + + // Encrypt with the peer's public key + let payload = FederationKeys.loadKey(peer.public_key, 'public').encrypt(stringPayload); + + // Encrypt with the local private key + payload = Federation.privateKey.encryptPrivate(payload); + + peerHTTP.request(peer, 'POST', '/api/v1/federation.events', { payload }, { total: 5, stepSize: 500, stepMultiplier: 10 }); + + FederationEvents.setEventAsFullfilled(e); + } catch (err) { + this.log(`[${ e.t }] Event could not be sent to peer:${ domain }`); + + if (err.response) { + const { response: { data: error } } = err; + + if (error.errorType === 'error-app-prevented-sending') { + const { payload: { + message: { + rid: roomId, + u: { + username, + federation: { _id: userId }, + }, + }, + } } = e; + + const localUsername = username.split('@')[0]; + + // Create system message + Messages.createRejectedMessageByPeer(roomId, localUsername, { + u: { + _id: userId, + username: localUsername, + }, + peer: domain, + }); + + return FederationEvents.setEventAsErrored(e, err.error, true); + } + } + + if (err.error === 'federation-peer-does-not-exist') { + const { payload: { + message: { + rid: roomId, + u: { + username, + federation: { _id: userId }, + }, + }, + } } = e; + + const localUsername = username.split('@')[0]; + + // Create system message + Messages.createPeerDoesNotExist(roomId, localUsername, { + u: { + _id: userId, + username: localUsername, + }, + peer: domain, + }); + + return FederationEvents.setEventAsErrored(e, err.error, true); + } + + return FederationEvents.setEventAsErrored(e, `Could not send request to ${ domain }`); + } + } + } + + onCreateEvent(e) { + this.propagateEvent(e); + } + + resendUnfulfilledEvents() { + // Should we use queues in here? + const events = FederationEvents.getUnfulfilled(); + + for (const e of events) { + this.propagateEvent(e); + } + } + + // ##### + // + // Users + // + // ##### + findUsers(email, options = {}) { + const [username, domain] = email.split('@'); + + const { peer: { domain: localPeerDomain } } = this; + + let peer = null; + + try { + peer = peerDNS.searchPeer(options.domainOverride || domain); + } catch (err) { + this.log(`Could not find peer using domain:${ domain }`); + throw new Meteor.Error('federation-peer-does-not-exist', `Could not find peer using domain:${ domain }`); + } + + try { + const { data: { federatedUsers: remoteFederatedUsers } } = peerHTTP.request(peer, 'GET', `/api/v1/federation.users?${ qs.stringify({ username, domain, emailOnly: options.emailOnly }) }`); + + const federatedUsers = []; + + for (const federatedUser of remoteFederatedUsers) { + federatedUsers.push(new FederatedUser(localPeerDomain, federatedUser.user)); + } + + return federatedUsers; + } catch (err) { + this.log(`Could not find user:${ username } at ${ peer.domain }`); + throw new Meteor.Error('federation-user-does-not-exist', `Could not find user:${ email } at ${ peer.domain }`); + } + } + + // ####### + // + // Uploads + // + // ####### + getUpload(options) { + const { identifier: domain, localMessage: { file: { _id: fileId } } } = options; + + let peer = null; + + try { + peer = peerDNS.searchPeer(domain); + } catch (err) { + this.log(`Could not find peer using domain:${ domain }`); + throw new Meteor.Error('federation-peer-does-not-exist', `Could not find peer using domain:${ domain }`); + } + + const { data: { upload, buffer } } = peerHTTP.request(peer, 'GET', `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); + + return { upload, buffer: Buffer.from(buffer) }; + } + + // ################# + // + // Callback handlers + // + // ################# + afterCreateDirectRoom(room, { from: owner }) { + this.log('afterCreateDirectRoom'); + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return room; } + + const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('afterCreateDirectRoom', federatedRoom.getLocalRoom())) { return room; } + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + + FederationEvents.directRoomCreated(federatedRoom, { skipPeers: [localPeerDomain] }); + } + + afterCreateRoom(roomOwner, room) { + this.log('afterCreateRoom'); + + const { _id: ownerId } = roomOwner; + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return roomOwner; } + + const owner = Users.findOneById(ownerId); + + const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('afterCreateRoom', federatedRoom.getLocalRoom())) { return roomOwner; } + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + + FederationEvents.roomCreated(federatedRoom, { skipPeers: [localPeerDomain] }); + } + + afterSaveRoomSettings(/* room */) { + this.log('afterSaveRoomSettings - NOT IMPLEMENTED'); + } + + afterAddedToRoom(users, room) { + this.log('afterAddedToRoom'); + + const { user: userWhoJoined, inviter: userWhoInvited } = users; + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('afterAddedToRoom', userWhoJoined)) { return users; } + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room, { checkUsingUsers: true })) { return users; } + + const extras = {}; + + // If the room is not federated and has an owner + if (!room.federation) { + let ownerId; + + // If the room does not have an owner, get the first user subscribed to that room + if (!room.u) { + const userSubscription = Subscriptions.findOne({ rid: room._id }, { + sort: { + ts: 1, + }, + }); + + ownerId = userSubscription.u._id; + } else { + ownerId = room.u._id; + } + + extras.owner = Users.findOneById(ownerId); + } + + const federatedRoom = new FederatedRoom(localPeerDomain, room, extras); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + + // If the user who joined is from a different peer... + if (userWhoJoined.federation && userWhoJoined.federation.peer !== localPeerDomain) { + // ...create a "create room" event for that peer + FederationEvents.roomCreated(federatedRoom, { peers: [userWhoJoined.federation.peer] }); + } + + // Then, create a "user join/added" event to the other peers + const federatedUserWhoJoined = FederatedUser.loadOrCreate(localPeerDomain, userWhoJoined); + + if (userWhoInvited) { + const federatedInviter = FederatedUser.loadOrCreate(localPeerDomain, userWhoInvited); + + FederationEvents.userAdded(federatedRoom, federatedUserWhoJoined, federatedInviter, { skipPeers: [localPeerDomain] }); + } else { + FederationEvents.userJoined(federatedRoom, federatedUserWhoJoined, { skipPeers: [localPeerDomain] }); + } + } + + beforeLeaveRoom(userWhoLeft, room) { + this.log('beforeLeaveRoom'); + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('beforeLeaveRoom', userWhoLeft)) { return userWhoLeft; } + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return userWhoLeft; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + const federatedUserWhoLeft = FederatedUser.loadByFederationId(localPeerDomain, userWhoLeft.federation._id); + + // Then, create a "user left" event to the other peers + FederationEvents.userLeft(federatedRoom, federatedUserWhoLeft, { skipPeers: [localPeerDomain] }); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + } + + beforeRemoveFromRoom(users, room) { + this.log('beforeRemoveFromRoom'); + + const { removedUser, userWhoRemoved } = users; + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('beforeRemoveFromRoom', removedUser)) { return users; } + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + const federatedRemovedUser = FederatedUser.loadByFederationId(localPeerDomain, removedUser.federation._id); + + const federatedUserWhoRemoved = FederatedUser.loadByFederationId(localPeerDomain, userWhoRemoved.federation._id); + + FederationEvents.userRemoved(federatedRoom, federatedRemovedUser, federatedUserWhoRemoved, { skipPeers: [localPeerDomain] }); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + } + + afterSaveMessage(message, room) { + this.log('afterSaveMessage'); + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('afterSaveMessage', message)) { return message; } + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + const federatedMessage = FederatedMessage.loadOrCreate(localPeerDomain, message); + + // If editedAt exists, it means it is an update + if (message.editedAt) { + const user = Users.findOneById(message.editedBy._id); + + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); + + FederationEvents.messageUpdated(federatedRoom, federatedMessage, federatedUser, { skipPeers: [localPeerDomain] }); + } else { + FederationEvents.messageCreated(federatedRoom, federatedMessage, { skipPeers: [localPeerDomain] }); + } + } + + afterDeleteMessage(message) { + this.log('afterDeleteMessage'); + + // Check if this should be skipped + if (this.skipCallbackIfNeeded('afterDeleteMessage', message)) { return message; } + + const { peer: { domain: localPeerDomain } } = this; + + const room = Rooms.findOneById(message.rid); + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + const federatedMessage = new FederatedMessage(localPeerDomain, message); + + FederationEvents.messageDeleted(federatedRoom, federatedMessage, { skipPeers: [localPeerDomain] }); + } + + afterReadMessages(roomId, { userId }) { + this.log('afterReadMessages'); + + if (!settings.get('Message_Read_Receipt_Enabled')) { this.log('Skipping: read receipts are not enabled'); return roomId; } + + const { peer: { domain: localPeerDomain } } = this; + + const room = Rooms.findOneById(roomId); + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return roomId; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + if (this.skipCallbackIfNeeded('afterReadMessages', federatedRoom.getLocalRoom())) { return roomId; } + + const user = Users.findOneById(userId); + + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); + + FederationEvents.messagesRead(federatedRoom, federatedUser, { skipPeers: [localPeerDomain] }); + } + + afterSetReaction(message, { user, reaction, shouldReact }) { + this.log('afterSetReaction'); + + const room = Rooms.findOneById(message.rid); + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } + + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); + + const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, message.federation._id); + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + FederationEvents.messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, { skipPeers: [localPeerDomain] }); + } + + afterUnsetReaction(message, { user, reaction, shouldReact }) { + this.log('afterUnsetReaction'); + + const room = Rooms.findOneById(message.rid); + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return message; } + + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, user.federation._id); + + const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, message.federation._id); + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + FederationEvents.messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, { skipPeers: [localPeerDomain] }); + } + + afterMuteUser(users, room) { + this.log('afterMuteUser'); + + const { mutedUser, fromUser } = users; + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + const federatedMutedUser = FederatedUser.loadByFederationId(localPeerDomain, mutedUser.federation._id); + + const federatedUserWhoMuted = FederatedUser.loadByFederationId(localPeerDomain, fromUser.federation._id); + + FederationEvents.userMuted(federatedRoom, federatedMutedUser, federatedUserWhoMuted, { skipPeers: [localPeerDomain] }); + } + + afterUnmuteUser(users, room) { + this.log('afterUnmuteUser'); + + const { unmutedUser, fromUser } = users; + + const { peer: { domain: localPeerDomain } } = this; + + // Check if room is federated + if (!FederatedRoom.isFederated(localPeerDomain, room)) { return users; } + + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, room.federation._id); + + const federatedUnmutedUser = FederatedUser.loadByFederationId(localPeerDomain, unmutedUser.federation._id); + + const federatedUserWhoUnmuted = FederatedUser.loadByFederationId(localPeerDomain, fromUser.federation._id); + + FederationEvents.userUnmuted(federatedRoom, federatedUnmutedUser, federatedUserWhoUnmuted, { skipPeers: [localPeerDomain] }); + } +} + +export default new PeerClient(); diff --git a/packages/rocketchat-federation/server/peerDNS.js b/packages/rocketchat-federation/server/peerDNS.js new file mode 100644 index 00000000000..4af0b03a2f6 --- /dev/null +++ b/packages/rocketchat-federation/server/peerDNS.js @@ -0,0 +1,175 @@ +import dns from 'dns'; +import { Meteor } from 'meteor/meteor'; +import { FederationDNSCache } from 'meteor/rocketchat:models'; + +import { logger } from './logger'; +import peerHTTP from './peerHTTP'; +import { updateStatus } from './settingsUpdater'; + +const dnsResolveSRV = Meteor.wrapAsync(dns.resolveSrv); +const dnsResolveTXT = Meteor.wrapAsync(dns.resolveTxt); + +class PeerDNS { + constructor() { + this.config = {}; + } + + setConfig(config) { + // General + this.config = config; + + // Setup HubPeer + const { hub: { url } } = config; + this.HubPeer = { url }; + } + + log(message) { + logger.dns.info(message); + } + + // ######## + // + // Register + // + // ######## + register(peerConfig) { + const { uniqueId, domain, url, public_key } = peerConfig; + + this.log(`Registering peer with domain ${ domain }...`); + + // Attempt to register peer + try { + peerHTTP.request(this.HubPeer, 'POST', '/api/v1/peers', { uniqueId, domain, url, public_key }, { total: 5, stepSize: 1000, tryToUpdateDNS: false }); + + this.log('Peer registered!'); + + updateStatus('Running, registered to Hub'); + + return true; + } catch (err) { + this.log(err); + + this.log('Could not register peer'); + + return false; + } + } + + // ############# + // + // Peer Handling + // + // ############# + searchPeer(domain) { + this.log(`searchPeer: ${ domain }`); + + let peer = FederationDNSCache.findOneByDomain(domain); + + // Try to lookup at the DNS Cache + if (!peer) { + this.updatePeerDNS(domain); + + peer = FederationDNSCache.findOneByDomain(domain); + } + + return peer; + } + + getPeerUsingDNS(domain) { + this.log(`getPeerUsingDNS: ${ domain }`); + + // Try searching by DNS first + const srvEntries = dnsResolveSRV(`_rocketchat._tcp.${ domain }`); + + const [srvEntry] = srvEntries; + + // Get the public key from the TXT record + const txtRecords = dnsResolveTXT(domain); + + let publicKey; + + for (const txtRecord of txtRecords) { + const joinedTxtRecord = txtRecord.join(''); + + if (joinedTxtRecord.indexOf('rocketchat-public-key=') === 0) { + publicKey = joinedTxtRecord; + break; + } + } + + if (!publicKey) { + throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records'); + } + + publicKey = publicKey.replace('rocketchat-public-key=', ''); + + const protocol = srvEntry.name === 'localhost' ? 'http' : 'https'; + + return { + domain, + url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`, + public_key: publicKey, + }; + } + + getPeerUsingHub(domain) { + this.log(`getPeerUsingHub: ${ domain }`); + + // If there is no DNS entry for that, get from the Hub + const { data: { peer } } = peerHTTP.simpleRequest(this.HubPeer, 'GET', `/api/v1/peers?search=${ domain }`); + + return peer; + } + + // ############## + // + // DNS Management + // + // ############## + updatePeerDNS(domain) { + this.log(`updatePeerDNS: ${ domain }`); + + let peer; + + try { + peer = this.getPeerUsingDNS(domain); + } catch (err) { + if (err.code !== 'ENOTFOUND') { + this.log(err); + + throw new Error(`Error trying to fetch SRV DNS entries for ${ domain }`); + } + + peer = this.getPeerUsingHub(domain); + } + + this.updateDNSCache.call(this, peer); + + return peer; + } + + updateDNSEntry(peer) { + this.log('updateDNSEntry'); + + const { domain } = peer; + + delete peer._id; + + // Make sure public_key has no line breaks + peer.public_key = peer.public_key.replace(/\n|\r/g, ''); + + return FederationDNSCache.upsert({ domain }, peer); + } + + updateDNSCache(peers) { + this.log('updateDNSCache'); + + peers = Array.isArray(peers) ? peers : [peers]; + + for (const peer of peers) { + this.updateDNSEntry.call(this, peer); + } + } +} + +export default new PeerDNS(); diff --git a/packages/rocketchat-federation/server/peerHTTP.js b/packages/rocketchat-federation/server/peerHTTP.js new file mode 100644 index 00000000000..26813b8ed84 --- /dev/null +++ b/packages/rocketchat-federation/server/peerHTTP.js @@ -0,0 +1,126 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; + +import { logger } from './logger'; +import peerDNS from './peerDNS'; + +// Should skip the retry if the error is one of the below? +const errorsToSkipRetrying = [ + 'error-app-prevented-sending', +]; + +function skipRetryOnSpecificError(err) { + return errorsToSkipRetrying.includes(err && err.errorType); +} + +// Delay method to wait a little bit before retrying +const delay = Meteor.wrapAsync(function(ms, callback) { + Meteor.setTimeout(function() { + callback(null); + }, ms); +}); + +function doSimpleRequest(peer, method, uri, body) { + this.log(`Request: ${ method } ${ uri }`); + + const { url: serverBaseURL } = peer; + + const url = `${ serverBaseURL }${ uri }`; + + let data = null; + + if (method === 'POST' || method === 'PUT') { + data = body; + } + + this.log(`Sending request: ${ method } - ${ uri }`); + + return HTTP.call(method, url, { data, timeout: 2000, headers: { 'x-federation-domain': this.config.peer.domain } }); +} + +// +// Actually does the request, handling retries and everything +function doRequest(peer, method, uri, body, retryInfo = {}) { + // Normalize retry info + retryInfo = { + total: retryInfo.total || 1, + stepSize: retryInfo.stepSize || 100, + stepMultiplier: retryInfo.stepMultiplier || 1, + tryToUpdateDNS: retryInfo.tryToUpdateDNS === undefined ? true : retryInfo.tryToUpdateDNS, + DNSUpdated: false, + }; + + for (let i = 0; i <= retryInfo.total; i++) { + try { + return doSimpleRequest.call(this, peer, method, uri, body); + } catch (err) { + try { + if (retryInfo.tryToUpdateDNS && !retryInfo.DNSUpdated) { + i--; + + retryInfo.DNSUpdated = true; + + this.log(`Trying to update local DNS cache for peer:${ peer.domain }`); + + peer = peerDNS.updatePeerDNS(peer.domain); + + continue; + } + } catch (err) { + if (err.response && err.response.statusCode === 404) { + throw new Meteor.Error('federation-peer-does-not-exist', 'Peer does not exist'); + } + } + + // Check if we need to skip due to specific error + if (skipRetryOnSpecificError(err && err.response && err.response.data)) { + this.log('Retry: skipping due to specific error'); + + throw err; + } + + if (i === retryInfo.total - 1) { + // Throw the error, as we could not fulfill the request + this.log('Retry: could not fulfill the request'); + + throw err; + } + + const timeToRetry = retryInfo.stepSize * (i + 1) * retryInfo.stepMultiplier; + + this.log(`Trying again in ${ timeToRetry / 1000 }s: ${ method } - ${ uri }`); + + // Otherwise, wait and try again + delay(timeToRetry); + } + } +} + +class PeerHTTP { + constructor() { + this.config = {}; + } + + setConfig(config) { + // General + this.config = config; + } + + log(message) { + logger.http.info(message); + } + + // + // Direct request + simpleRequest(peer, method, uri, body) { + return doSimpleRequest.call(this, peer, method, uri, body); + } + + // + // Request trying to find DNS entries + request(peer, method, uri, body, retryInfo = {}) { + return doRequest.call(this, peer, method, uri, body, retryInfo); + } +} + +export default new PeerHTTP(); diff --git a/packages/rocketchat-federation/server/peerServer/index.js b/packages/rocketchat-federation/server/peerServer/index.js new file mode 100644 index 00000000000..d8144d71ed1 --- /dev/null +++ b/packages/rocketchat-federation/server/peerServer/index.js @@ -0,0 +1,8 @@ +import peerServer from './peerServer'; + +// Setup routes +import './routes/events'; +import './routes/uploads'; +import './routes/users'; + +export default peerServer; diff --git a/packages/rocketchat-federation/server/peerServer/peerServer.js b/packages/rocketchat-federation/server/peerServer/peerServer.js new file mode 100644 index 00000000000..96db2a634e0 --- /dev/null +++ b/packages/rocketchat-federation/server/peerServer/peerServer.js @@ -0,0 +1,388 @@ +import { callbacks } from 'meteor/rocketchat:callbacks'; +import { setReaction } from 'meteor/rocketchat:reactions'; +import { addUserToRoom, removeUserFromRoom, deleteMessage } from 'meteor/rocketchat:lib'; +import { Rooms, Subscriptions } from 'meteor/rocketchat:models'; + +import { FederatedMessage, FederatedRoom, FederatedUser } from '../federatedResources'; +import { logger } from '../logger.js'; +import peerClient from '../peerClient'; + +class PeerServer { + constructor() { + this.config = {}; + this.enabled = false; + } + + setConfig(config) { + // General + this.config = config; + } + + log(message) { + logger.peerServer.info(message); + } + + disable() { + this.log('Disabling...'); + + this.enabled = false; + } + + enable() { + this.log('Enabling...'); + + this.enabled = true; + } + + start() { + this.log('Routes are set'); + } + + handleDirectRoomCreatedEvent(e) { + this.log('handleDirectRoomCreatedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { room, owner, users } } = e; + + // Load the federated room + const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); + + // Set users + federatedRoom.setUsers(users); + + // Create, if needed, all room's users + federatedRoom.createUsers(); + + // Then, create the room, if needed + federatedRoom.create(); + } + + handleRoomCreatedEvent(e) { + this.log('handleRoomCreatedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { room, owner, users } } = e; + + // Load the federated room + const federatedRoom = new FederatedRoom(localPeerDomain, room, { owner }); + + // Set users + federatedRoom.setUsers(users); + + // Create, if needed, all room's users + federatedRoom.createUsers(); + + // Then, create the room, if needed + federatedRoom.create(); + } + + handleUserJoinedEvent(e) { + this.log('handleUserJoinedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, user } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + // Create the user, if needed + const federatedUser = FederatedUser.loadOrCreate(localPeerDomain, user); + const localUser = federatedUser.create(); + + // Callback management + peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); + + // Add the user to the room + addUserToRoom(federatedRoom.room._id, localUser, null, false); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + } + + handleUserAddedEvent(e) { + this.log('handleUserAddedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_inviter_id, user } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + // Load the inviter + const federatedInviter = FederatedUser.loadByFederationId(localPeerDomain, federated_inviter_id); + + if (!federatedInviter) { + throw new Error('Inviting user does not exist'); + } + + const localInviter = federatedInviter.getLocalUser(); + + // Create the user, if needed + const federatedUser = FederatedUser.loadOrCreate(localPeerDomain, user); + const localUser = federatedUser.create(); + + // Callback management + peerClient.addCallbackToSkip('afterAddedToRoom', federatedUser.getFederationId()); + + // Add the user to the room + addUserToRoom(federatedRoom.room._id, localUser, localInviter, false); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + } + + handleUserLeftEvent(e) { + this.log('handleUserLeftEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_user_id } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + // Load the user who left + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // Callback management + peerClient.addCallbackToSkip('beforeLeaveRoom', federatedUser.getFederationId()); + + // Remove the user from the room + removeUserFromRoom(federatedRoom.room._id, localUser); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + } + + handleUserRemovedEvent(e) { + this.log('handleUserRemovedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_user_id, federated_removed_by_user_id } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + // Load the user who left + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // Load the user who removed + const federatedUserWhoRemoved = FederatedUser.loadByFederationId(localPeerDomain, federated_removed_by_user_id); + const localUserWhoRemoved = federatedUserWhoRemoved.getLocalUser(); + + // Callback management + peerClient.addCallbackToSkip('beforeRemoveFromRoom', federatedUser.getFederationId()); + + // Remove the user from the room + removeUserFromRoom(federatedRoom.room._id, localUser, { byUser: localUserWhoRemoved }); + + // Load federated users + federatedRoom.loadUsers(); + + // Refresh room's federation + federatedRoom.refreshFederation(); + } + + handleUserMutedEvent(e) { + this.log('handleUserMutedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_user_id } } = e; + // const { payload: { federated_room_id, federated_user_id, federated_muted_by_user_id } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + // Load the user who left + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // // Load the user who muted + // const federatedUserWhoMuted = FederatedUser.loadByFederationId(localPeerDomain, federated_muted_by_user_id); + // const localUserWhoMuted = federatedUserWhoMuted.getLocalUser(); + + // Mute user + Rooms.muteUsernameByRoomId(federatedRoom.room._id, localUser.username); + + // TODO: should we create a message? + } + + handleUserUnmutedEvent(e) { + this.log('handleUserUnmutedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_user_id } } = e; + // const { payload: { federated_room_id, federated_user_id, federated_unmuted_by_user_id } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + // Load the user who left + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // // Load the user who muted + // const federatedUserWhoUnmuted = FederatedUser.loadByFederationId(localPeerDomain, federated_unmuted_by_user_id); + // const localUserWhoUnmuted = federatedUserWhoUnmuted.getLocalUser(); + + // Unmute user + Rooms.unmuteUsernameByRoomId(federatedRoom.room._id, localUser.username); + + // TODO: should we create a message? + } + + handleMessageCreatedEvent(e) { + this.log('handleMessageCreatedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { message } } = e; + + // Load the federated message + const federatedMessage = new FederatedMessage(localPeerDomain, message); + + // Callback management + peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); + + // Create the federated message + federatedMessage.create(); + } + + handleMessageUpdatedEvent(e) { + this.log('handleMessageUpdatedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { message, federated_user_id } } = e; + + // Load the federated message + const federatedMessage = new FederatedMessage(localPeerDomain, message); + + // Load the federated user + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + + // Callback management + peerClient.addCallbackToSkip('afterSaveMessage', federatedMessage.getFederationId()); + + // Update the federated message + federatedMessage.update(federatedUser); + } + + handleMessageDeletedEvent(e) { + this.log('handleMessageDeletedEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_message_id } } = e; + + const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id); + + // Load the federated message + const localMessage = federatedMessage.getLocalMessage(); + + // Load the author + const localAuthor = federatedMessage.federatedAuthor.getLocalUser(); + + // Callback management + peerClient.addCallbackToSkip('afterDeleteMessage', federatedMessage.getFederationId()); + + // Create the federated message + deleteMessage(localMessage, localAuthor); + } + + handleMessagesReadEvent(e) { + this.log('handleMessagesReadEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_user_id } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + + peerClient.addCallbackToSkip('afterReadMessages', federatedRoom.getFederationId()); + + // Load the user who left + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // Mark the messages as read + // TODO: move below calls to an exported function + const userSubscription = Subscriptions.findOneByRoomIdAndUserId(federatedRoom.room._id, localUser._id, { fields: { ls: 1 } }); + Subscriptions.setAsReadByRoomIdAndUserId(federatedRoom.room._id, localUser._id); + + callbacks.run('afterReadMessages', federatedRoom.room._id, { userId: localUser._id, lastSeen: userSubscription.ls }); + } + + handleMessagesSetReactionEvent(e) { + this.log('handleMessagesSetReactionEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_message_id, federated_user_id, reaction, shouldReact } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + const localRoom = federatedRoom.getLocalRoom(); + + // Load the user who reacted + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // Load the message + const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id); + const localMessage = federatedMessage.getLocalMessage(); + + // Callback management + peerClient.addCallbackToSkip('afterSetReaction', federatedMessage.getFederationId()); + + // Set message reaction + setReaction(localRoom, localUser, localMessage, reaction, shouldReact); + } + + handleMessagesUnsetReactionEvent(e) { + this.log('handleMessagesUnsetReactionEvent'); + + const { peer: { domain: localPeerDomain } } = this.config; + + const { payload: { federated_room_id, federated_message_id, federated_user_id, reaction, shouldReact } } = e; + + // Load the federated room + const federatedRoom = FederatedRoom.loadByFederationId(localPeerDomain, federated_room_id); + const localRoom = federatedRoom.getLocalRoom(); + + // Load the user who reacted + const federatedUser = FederatedUser.loadByFederationId(localPeerDomain, federated_user_id); + const localUser = federatedUser.getLocalUser(); + + // Load the message + const federatedMessage = FederatedMessage.loadByFederationId(localPeerDomain, federated_message_id); + const localMessage = federatedMessage.getLocalMessage(); + + // Callback management + peerClient.addCallbackToSkip('afterUnsetReaction', federatedMessage.getFederationId()); + + // Unset message reaction + setReaction(localRoom, localUser, localMessage, reaction, shouldReact); + } +} + +export default new PeerServer(); diff --git a/packages/rocketchat-federation/server/peerServer/routes/events.js b/packages/rocketchat-federation/server/peerServer/routes/events.js new file mode 100644 index 00000000000..252ad6ec754 --- /dev/null +++ b/packages/rocketchat-federation/server/peerServer/routes/events.js @@ -0,0 +1,106 @@ +import { API } from 'meteor/rocketchat:api'; +import { FederationKeys } from 'meteor/rocketchat:models'; + +import { Federation } from '../../main'; + +import peerDNS from '../../peerDNS'; +import peerServer from '../peerServer'; + +API.v1.addRoute('federation.events', { authRequired: false }, { + post() { + if (!peerServer.enabled) { + return API.v1.failure('Not found'); + } + + if (!this.bodyParams.payload) { + return API.v1.failure('Payload was not sent'); + } + + if (!this.request.headers['x-federation-domain']) { + return API.v1.failure('Cannot handle that request'); + } + + const remotePeerDomain = this.request.headers['x-federation-domain']; + + const peer = peerDNS.searchPeer(remotePeerDomain); + + if (!peer) { + return API.v1.failure('Could not find valid peer'); + } + + const payloadBuffer = Buffer.from(this.bodyParams.payload.data); + + // Decrypt with the peer's public key + let payload = FederationKeys.loadKey(peer.public_key, 'public').decryptPublic(payloadBuffer); + + // Decrypt with the local private key + payload = Federation.privateKey.decrypt(payload); + + // Get the event + const { event: e } = JSON.parse(payload.toString()); + + if (!e) { + return API.v1.failure('Event was not sent'); + } + + peerServer.log(`Received event:${ e.t }`); + + try { + switch (e.t) { + case 'drc': + peerServer.handleDirectRoomCreatedEvent(e); + break; + case 'roc': + peerServer.handleRoomCreatedEvent(e); + break; + case 'usj': + peerServer.handleUserJoinedEvent(e); + break; + case 'usa': + peerServer.handleUserAddedEvent(e); + break; + case 'usl': + peerServer.handleUserLeftEvent(e); + break; + case 'usr': + peerServer.handleUserRemovedEvent(e); + break; + case 'usm': + peerServer.handleUserMutedEvent(e); + break; + case 'usu': + peerServer.handleUserUnmutedEvent(e); + break; + case 'msc': + peerServer.handleMessageCreatedEvent(e); + break; + case 'msu': + peerServer.handleMessageUpdatedEvent(e); + break; + case 'msd': + peerServer.handleMessageDeletedEvent(e); + break; + case 'msr': + peerServer.handleMessagesReadEvent(e); + break; + case 'mrs': + peerServer.handleMessagesSetReactionEvent(e); + break; + case 'mru': + peerServer.handleMessagesUnsetReactionEvent(e); + break; + default: + throw new Error(`Invalid event:${ e.t }`); + } + + peerServer.log('Success, responding...'); + + // Respond + return API.v1.success(); + } catch (err) { + peerServer.log(`Error handling event:${ e.t } - ${ err.toString() }`); + + return API.v1.failure(`Error handling event:${ e.t } - ${ err.toString() }`, err.error || 'unknown-error'); + } + }, +}); diff --git a/packages/rocketchat-federation/server/peerServer/routes/uploads.js b/packages/rocketchat-federation/server/peerServer/routes/uploads.js new file mode 100644 index 00000000000..4233e859fb3 --- /dev/null +++ b/packages/rocketchat-federation/server/peerServer/routes/uploads.js @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; +import { API } from 'meteor/rocketchat:api'; +import { Uploads } from 'meteor/rocketchat:models'; +import { FileUpload } from 'meteor/rocketchat:file-upload'; + +import peerServer from '../peerServer'; + +API.v1.addRoute('federation.uploads', { authRequired: false }, { + get() { + if (!peerServer.enabled) { + return API.v1.failure('Not found'); + } + + const { upload_id } = this.requestParams(); + + const upload = Uploads.findOneById(upload_id); + + if (!upload) { + return API.v1.failure('There is no such file in this server'); + } + + const getFileBuffer = Meteor.wrapAsync(FileUpload.getBuffer, FileUpload); + + const buffer = getFileBuffer(upload); + + return API.v1.success({ upload, buffer }); + }, +}); diff --git a/packages/rocketchat-federation/server/peerServer/routes/users.js b/packages/rocketchat-federation/server/peerServer/routes/users.js new file mode 100644 index 00000000000..f81731d2cb3 --- /dev/null +++ b/packages/rocketchat-federation/server/peerServer/routes/users.js @@ -0,0 +1,49 @@ +import { API } from 'meteor/rocketchat:api'; +import { Users } from 'meteor/rocketchat:models'; + +import { FederatedUser } from '../../federatedResources'; +import peerServer from '../peerServer'; + +API.v1.addRoute('federation.users', { authRequired: false }, { + get() { + if (!peerServer.enabled) { + return API.v1.failure('Not found'); + } + + const { peer: { domain: localPeerDomain } } = peerServer.config; + + const { username, domain, emailOnly } = this.requestParams(); + + const email = `${ username }@${ domain }`; + + peerServer.log(`[users] Trying to find user by username:${ username } and email:${ email }`); + + const query = { + type: 'user', + }; + + if (emailOnly === 'true') { + query['emails.address'] = email; + } else { + query.$or = [ + { name: username }, + { username }, + { 'emails.address': email }, + ]; + } + + const users = Users.find(query, { fields: { services: 0, roles: 0 } }).fetch(); + + if (!users.length) { + return API.v1.failure('There is no such user in this server'); + } + + const federatedUsers = []; + + for (const user of users) { + federatedUsers.push(new FederatedUser(localPeerDomain, user)); + } + + return API.v1.success({ federatedUsers }); + }, +}); diff --git a/packages/rocketchat-federation/server/settingsUpdater.js b/packages/rocketchat-federation/server/settingsUpdater.js new file mode 100644 index 00000000000..81d96f16045 --- /dev/null +++ b/packages/rocketchat-federation/server/settingsUpdater.js @@ -0,0 +1,17 @@ +import { Settings } from 'meteor/rocketchat:models'; + +let nextStatus; + +export function updateStatus(status) { + Settings.updateValueById('FEDERATION_Status', nextStatus || status); + + nextStatus = null; +} + +export function updateNextStatusTo(status) { + nextStatus = status; +} + +export function updateEnabled(enabled) { + Settings.updateValueById('FEDERATION_Enabled', enabled); +} diff --git a/packages/rocketchat-file-upload/lib/FileUpload.js b/packages/rocketchat-file-upload/lib/FileUpload.js index 82e628f626d..b7abf01991a 100644 --- a/packages/rocketchat-file-upload/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/lib/FileUpload.js @@ -14,8 +14,10 @@ export const FileUpload = { if (!Match.test(file.rid, String)) { return false; } + // livechat users can upload files but they don't have an userId - const user = file.userId ? Meteor.user() : null; + const user = file.userId ? Meteor.users.findOne(file.userId) : null; + const room = Rooms.findOneById(file.rid); const directMessageAllow = settings.get('FileUpload_Enabled_Direct'); const fileUploadAllowed = settings.get('FileUpload_Enabled'); diff --git a/packages/rocketchat-file-upload/server/lib/FileUpload.js b/packages/rocketchat-file-upload/server/lib/FileUpload.js index da6a7358d8a..61360bb3170 100644 --- a/packages/rocketchat-file-upload/server/lib/FileUpload.js +++ b/packages/rocketchat-file-upload/server/lib/FileUpload.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import fs from 'fs'; import stream from 'stream'; +import streamBuffers from 'stream-buffers'; import mime from 'mime-type/with-db'; import Future from 'fibers/future'; import sharp from 'sharp'; @@ -274,6 +275,22 @@ export const FileUpload = Object.assign(_FileUpload, { res.end(); }, + getBuffer(file, cb) { + const store = this.getStoreByName(file.store); + + if (!store || !store.get) { cb(new Error('Store is invalid'), null); } + + const buffer = new streamBuffers.WritableStreamBuffer({ + initialSize: file.size, + }); + + buffer.on('finish', () => { + cb(null, buffer.getContents()); + }); + + store.copy(file, buffer); + }, + copy(file, targetFile) { const store = this.getStoreByName(file.store); const out = fs.createWriteStream(targetFile); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index d9bbff22ef9..800e8d2e4db 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1250,6 +1250,7 @@ "every_hour": "Once every hour", "every_six_hours": "Once every six hours", "every_day": "Once every day", + "Every_Workspace": "Every Workspace", "Everyone_can_access_this_channel": "Everyone can access this channel", "Example_s": "Example: %s", "Exclude_Botnames": "Exclude Bots", @@ -1270,6 +1271,20 @@ "Favorites": "Favorites", "Feature_Depends_on_Livechat_Visitor_navigation_as_a_message_to_be_enabled": "This feature depends on \"Send Visitor Navigation History as a Message\" to be enabled.", "Features_Enabled": "Features Enabled", + "FEDERATION_Enabled": "Attempt to integrate federation support. Changing this value requires restarting Rocket.Chat.", + "FEDERATION_Enabled_Alert": "Federation Support is a work in progress. Use on a production system is not recommended at this time.", + "FEDERATION_Discovery_Method": "Discovery Method", + "FEDERATION_Discovery_Method_Description": "You can use the hub or a SRV and a TXT entry on your DNS records.", + "FEDERATION_Domain": "Domain", + "FEDERATION_Domain_Description": "Add the domain that this server should be linked to - for example: @rocket.chat.", + "FEDERATION_Domain_Alert": "Do not change this after enabling the feature, we can't handle domain changes yet.", + "FEDERATION_Public_Key": "Public Key", + "FEDERATION_Public_Key_Description": "This is the key you need to share with your peers.", + "FEDERATION_Hub_URL": "Hub URL", + "FEDERATION_Hub_URL_Description": "Set the hub URL, for example: https://hub.rocket.chat. Ports are accepted as well.", + "FEDERATION_Unique_Id": "Unique ID", + "FEDERATION_Unique_Id_Description": "This is your federation unique ID, used to identify your peer on the mesh.", + "FEDERATION_Status": "Status", "Field": "Field", "Field_removed": "Field removed", "Field_required": "Field required", @@ -1816,6 +1831,7 @@ "Loading_more_from_history": "Loading more from history", "Loading_suggestion": "Loading suggestions", "Local_Password": "Local Password", + "Local_Workspace": "Local Workspace", "Localization": "Localization", "Log_Exceptions_to_Channel_Description": "A channel that will receive all captured exceptions. Leave empty to ignore exceptions.", "Log_Exceptions_to_Channel": "Log Exceptions to Channel", @@ -2692,6 +2708,7 @@ "The_emails_are_being_sent": "The emails are being sent.", "The_field_is_required": "The field %s is required.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "The image resize will not work because we can not detect ImageMagick or GraphicsMagick installed on your server.", + "The_peer__peer__does_not_exist": "The peer __peer__ does not exist.", "The_redirectUri_is_required": "The redirectUri is required", "The_server_will_restart_in_s_seconds": "The server will restart in %s seconds", "The_setting_s_is_configured_to_s_and_you_are_accessing_from_s": "The setting %s is configured to %s and you are accessing from %s!", @@ -2760,6 +2777,7 @@ "This_email_has_already_been_used_and_has_not_been_verified__Please_change_your_password": "This email has already been used and has not been verified. Please change your password.", "This_is_a_desktop_notification": "This is a desktop notification", "This_is_a_push_test_messsage": "This is a push test message", + "This_message_was_rejected_by__peer__peer": "This message was rejected by __peer__ peer.", "This_month": "This Month", "This_room_has_been_archived_by__username_": "This room has been archived by __username__", "This_room_has_been_unarchived_by__username_": "This room has been unarchived by __username__", diff --git a/packages/rocketchat-lib/server/functions/addUserToRoom.js b/packages/rocketchat-lib/server/functions/addUserToRoom.js index 9efa0602d8b..2dc06448e91 100644 --- a/packages/rocketchat-lib/server/functions/addUserToRoom.js +++ b/packages/rocketchat-lib/server/functions/addUserToRoom.js @@ -14,6 +14,10 @@ export const addUserToRoom = function(rid, user, inviter, silenced) { } if (room.t === 'c' || room.t === 'p') { + // Add a new event, with an optional inviter + callbacks.run('beforeAddedToRoom', { user, inviter }, room); + + // Keep the current event callbacks.run('beforeJoinRoom', user, room); } @@ -47,6 +51,10 @@ export const addUserToRoom = function(rid, user, inviter, silenced) { if (room.t === 'c' || room.t === 'p') { Meteor.defer(function() { + // Add a new event, with an optional inviter + callbacks.run('afterAddedToRoom', { user, inviter }, room); + + // Keep the current event callbacks.run('afterJoinRoom', user, room); }); } diff --git a/packages/rocketchat-lib/server/functions/createRoom.js b/packages/rocketchat-lib/server/functions/createRoom.js index 86a75ac1e64..80daa229fcb 100644 --- a/packages/rocketchat-lib/server/functions/createRoom.js +++ b/packages/rocketchat-lib/server/functions/createRoom.js @@ -7,7 +7,57 @@ import { Apps } from 'meteor/rocketchat:apps'; import _ from 'underscore'; import s from 'underscore.string'; -export const createRoom = function(type, name, owner, members, readOnly, extraData = {}) { +function createDirectRoom(source, target, extraData, options) { + const rid = [source._id, target._id].sort().join(''); + + Rooms.upsert({ _id: rid }, { + $setOnInsert: Object.assign({ + t: 'd', + usernames: [source.username, target.username], + msgs: 0, + ts: new Date(), + }, extraData), + }); + + Subscriptions.upsert({ rid, 'u._id': target._id }, { + $setOnInsert: Object.assign({ + name: source.username, + t: 'd', + open: true, + alert: true, + unread: 0, + u: { + _id: target._id, + username: target.username, + }, + }, options.subscriptionExtra), + }); + + Subscriptions.upsert({ rid, 'u._id': source._id }, { + $setOnInsert: Object.assign({ + name: target.username, + t: 'd', + open: true, + alert: true, + unread: 0, + u: { + _id: source._id, + username: source.username, + }, + }, options.subscriptionExtra), + }); + + return { + _id: rid, + t: 'd', + }; +} + +export const createRoom = function(type, name, owner, members, readOnly, extraData = {}, options = {}) { + if (type === 'd') { + return createDirectRoom(members[0], members[1], extraData, options); + } + name = s.trim(name); owner = s.trim(owner); members = [].concat(members); @@ -31,8 +81,15 @@ export const createRoom = function(type, name, owner, members, readOnly, extraDa } const now = new Date(); + + const validRoomNameOptions = {}; + + if (options.nameValidationRegex) { + validRoomNameOptions.nameValidationRegex = options.nameValidationRegex; + } + let room = Object.assign({ - name: getValidRoomName(name), + name: getValidRoomName(name, null, validRoomNameOptions), fname: name, t: type, msgs: 0, @@ -84,7 +141,9 @@ export const createRoom = function(type, name, owner, members, readOnly, extraDa Rooms.muteUsernameByRoomId(room._id, username); } - const extra = { open: true }; + const extra = options.subscriptionExtra || {}; + + extra.open = true; if (username === owner.username) { extra.ls = now; diff --git a/packages/rocketchat-lib/server/functions/removeUserFromRoom.js b/packages/rocketchat-lib/server/functions/removeUserFromRoom.js index 6c623e53aa3..07fbdb675a8 100644 --- a/packages/rocketchat-lib/server/functions/removeUserFromRoom.js +++ b/packages/rocketchat-lib/server/functions/removeUserFromRoom.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Rooms, Messages, Subscriptions } from 'meteor/rocketchat:models'; import { callbacks } from 'meteor/rocketchat:callbacks'; -export const removeUserFromRoom = function(rid, user) { +export const removeUserFromRoom = function(rid, user, options = {}) { const room = Rooms.findOneById(rid); if (room) { @@ -12,7 +12,13 @@ export const removeUserFromRoom = function(rid, user) { if (subscription) { const removedUser = user; - Messages.createUserLeaveWithRoomIdAndUser(rid, removedUser); + if (options.byUser) { + Messages.createUserRemovedWithRoomIdAndUser(rid, user, { + u: options.byUser, + }); + } else { + Messages.createUserLeaveWithRoomIdAndUser(rid, removedUser); + } } if (room.t === 'l') { diff --git a/packages/rocketchat-livechat/.app/package-lock.json b/packages/rocketchat-livechat/.app/package-lock.json index 5330eff4f5d..db363ce6d51 100644 --- a/packages/rocketchat-livechat/.app/package-lock.json +++ b/packages/rocketchat-livechat/.app/package-lock.json @@ -12,24 +12,6 @@ "regenerator-runtime": "^0.12.0" } }, - "asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "assert": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "requires": { - "util": "0.10.3" - } - }, "autolinker": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-1.8.1.tgz", @@ -40,11 +22,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, - "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" - }, "bcrypt": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.2.tgz", @@ -475,11 +452,6 @@ } } }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -489,123 +461,11 @@ "concat-map": "0.0.1" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", - "requires": { - "pako": "~0.2.0" - } - }, - "buffer": { - "version": "4.9.1", - "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "requires": { - "date-now": "^0.1.4" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" - }, "core-js": { "version": "2.5.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", @@ -616,175 +476,11 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" - }, - "elliptic": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", - "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "events": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "https-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", - "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" - }, - "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - }, "inherits-ex": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/inherits-ex/-/inherits-ex-1.2.3.tgz", @@ -793,26 +489,11 @@ "xtend": "^4.0.0" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, "jquery": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -848,11 +529,39 @@ "vm-browserify": "0.0.4" }, "dependencies": { + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "requires": { + "util": "0.10.3" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz", + "integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -862,11 +571,230 @@ "concat-map": "0.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browserify-aes": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.1.1.tgz", + "integrity": "sha512-UGnTYAnB2a3YuYKIRy1/4FB2HdM866E0qC46JXvVTYKlBlZlnvfpSfY6OKfXZAkv70eJ2a1SqzpAo5CRhZGDFg==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.0.tgz", + "integrity": "sha1-mYgkSHS/XtTijalWZtzWasj8Njo=", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.0.tgz", + "integrity": "sha1-2qJ3cXRwki7S/hhZQRihdUOXId0=", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=", + "requires": { + "pako": "~0.2.0" + } + }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "create-ecdh": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", + "integrity": "sha1-iIxyNZbN92EvZJgjPuvXo1MBc30=", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha1-YGBCrIuSYnUPSDyt2rD1gZFy2P0=", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.6.tgz", + "integrity": "sha1-rLniIaThe9sHbpBlfEK5PjcmzwY=", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=" + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "diffie-hellman": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.2.tgz", + "integrity": "sha1-tYNXOScM/ias9jIJn97SoH8gnl4=", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "elliptic": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", + "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -885,6 +813,55 @@ "path-is-absolute": "^1.0.0" } }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha1-ZuodhW206KVHDK32/OI65SRO8uE=", + "requires": { + "inherits": "^2.0.1" + } + }, + "hash.js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", + "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + } + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "https-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.1.tgz", + "integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=" + }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -894,6 +871,55 @@ "wrappy": "1" } }, + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + }, + "dependencies": { + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + } + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "minimalistic-assert": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", + "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -910,11 +936,133 @@ "wrappy": "1" } }, + "os-browserify": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", + "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" + }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, + "parse-asn1": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.0.tgz", + "integrity": "sha1-N8T5t+06tlx0gXtfJICTf7+XxxI=", + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "path-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", + "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" + }, "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=" }, + "pbkdf2": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.14.tgz", + "integrity": "sha512-gjsZW9O34fm0R7PaLHRJmLLVfSoesxztjPjE9o6R+qtVJij90ltg1joIovN9GKrRW3t1PzhDDG3UMEMFfZ+1wA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "public-encrypt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", + "integrity": "sha1-OfaZ86RlYN1eusvKaTyvfGXBjMY=", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, "rimraf": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", @@ -923,22 +1071,125 @@ "glob": "^7.0.5" } }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha1-D0WEKVxTo2KK9+bXmsohzlfRxuc=", + "requires": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "sha.js": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.10.tgz", + "integrity": "sha512-vnwmrFDlOExK4Nm16J2KMWHLrp14lBrjxMxBJpu++EnsuBmpiYaM/MEs46Vxxm/4FvdP5yTwuCTO9it5FSjrqA==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.0.tgz", + "integrity": "sha512-sZOFxI/5xw058XIRHl4dU3dZ+TTOIGJR78Dvo0oEAejIt4ou27k+3ne1zYmCV+v7UucbxIFQuOgnkTVHh8YPnw==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.3", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "string_decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.0.tgz", + "integrity": "sha512-8zQpRF6juocE69ae7CSPmYEGJe4VCXwP6S6dxUWI7i53Gwv54/ec41fiUA+X7BPGGv7fRSQJjBQVa0gomGaOgg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "requires": { + "process": "~0.11.0" + } + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "requires": { + "indexof": "0.0.1" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" } } }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, "mime-db": { "version": "1.37.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", @@ -955,16 +1206,6 @@ "util-ex": "^0.3.15" } }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -983,33 +1224,6 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==" }, - "os-browserify": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.2.1.tgz", - "integrity": "sha1-Y/xMzuXS13Y9Jrv4YBB45sLgBE8=" - }, - "pako": { - "version": "0.2.9", - "resolved": "http://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" - }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", - "requires": { - "asn1.js": "^4.0.0", - "browserify-aes": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" - } - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=" - }, "path.js": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path.js/-/path.js-1.0.7.tgz", @@ -1020,174 +1234,21 @@ "util-ex": "^0.3.10" } }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, "regenerator-runtime": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "sprintf-js": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.1.tgz", "integrity": "sha1-Nr54Mgr+WAH2zqPueLblqrlA6gw=" }, - "stream-browserify": { - "version": "2.0.1", - "resolved": "http://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-http": { - "version": "2.8.1", - "resolved": "http://registry.npmjs.org/stream-http/-/stream-http-2.8.1.tgz", - "integrity": "sha512-cQ0jo17BLca2r0GfRdZKYAGLU6JRoIWxqSOakUMuKOT6MOK7AAlE856L33QuDmAy/eeOrhLee3dZKX0Uadu93A==", - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.3", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, "sweetalert2": { "version": "7.29.2", "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-7.29.2.tgz", "integrity": "sha512-p+Zp2ly8vf9jGlzlUOmpVDZZoRHHatTHa3H3OnXVInQKJ5HHQr5Vg8XnledhS4Iih6TdSadbdGZ8Y4gE+OUgOQ==" }, - "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", - "requires": { - "process": "~0.11.0" - } - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=" - }, "toastr": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", @@ -1196,11 +1257,6 @@ "jquery": ">=1.12.0" } }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" - }, "underscore": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", @@ -1215,30 +1271,6 @@ "util-deprecate": "^1.0.2" } }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "util": { - "version": "0.10.3", - "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - } - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -1253,14 +1285,6 @@ "xtend": "^4.0.0" } }, - "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "requires": { - "indexof": "0.0.1" - } - }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/packages/rocketchat-mentions/lib/Mentions.js b/packages/rocketchat-mentions/lib/Mentions.js index 92feea80b94..3e33d29f5e0 100644 --- a/packages/rocketchat-mentions/lib/Mentions.js +++ b/packages/rocketchat-mentions/lib/Mentions.js @@ -28,10 +28,10 @@ export default class { return typeof this._useRealName === 'function' ? this._useRealName() : this._useRealName; } get userMentionRegex() { - return new RegExp(`(^|\\s|

|
?)@(${ this.pattern })`, 'gm'); + return new RegExp(`(^|\\s|

|
?)@(${ this.pattern }(@(${ this.pattern }))?)`, 'gm'); } get channelMentionRegex() { - return new RegExp(`(^|\\s|

)#(${ this.pattern })`, 'gm'); + return new RegExp(`(^|\\s|

)#(${ this.pattern }(@(${ this.pattern }))?)`, 'gm'); } replaceUsers(str, message, me) { return str.replace(this.userMentionRegex, (match, prefix, username) => { @@ -40,9 +40,11 @@ export default class { } const mentionObj = message.mentions && message.mentions.find((m) => m.username === username); + if (message.temp == null && mentionObj == null) { return match; } + const name = this.useRealName && mentionObj && s.escapeHTML(mentionObj.name); return `${ prefix }${ name || `@${ username }` }`; diff --git a/packages/rocketchat-models/server/index.js b/packages/rocketchat-models/server/index.js index d1673f0ef45..af01b934c23 100644 --- a/packages/rocketchat-models/server/index.js +++ b/packages/rocketchat-models/server/index.js @@ -35,6 +35,9 @@ import ReadReceipts from './models/ReadReceipts'; export { AppsLogsModel } from './models/apps-logs-model'; export { AppsPersistenceModel } from './models/apps-persistence-model'; export { AppsModel } from './models/apps-model'; +export { FederationDNSCache } from './models/FederationDNSCache'; +export { FederationEvents } from './models/FederationEvents'; +export { FederationKeys } from './models/FederationKeys'; export { Base, diff --git a/packages/rocketchat-models/server/models/FederationDNSCache.js b/packages/rocketchat-models/server/models/FederationDNSCache.js new file mode 100644 index 00000000000..155deed53b9 --- /dev/null +++ b/packages/rocketchat-models/server/models/FederationDNSCache.js @@ -0,0 +1,13 @@ +import { Base } from './_Base'; + +class FederationDNSCacheModel extends Base { + constructor() { + super('federation_dns_cache'); + } + + findOneByDomain(domain) { + return this.findOne({ domain }); + } +} + +export const FederationDNSCache = new FederationDNSCacheModel(); diff --git a/packages/rocketchat-models/server/models/FederationEvents.js b/packages/rocketchat-models/server/models/FederationEvents.js new file mode 100644 index 00000000000..84c7bfbf565 --- /dev/null +++ b/packages/rocketchat-models/server/models/FederationEvents.js @@ -0,0 +1,255 @@ +import { Base } from './_Base'; + +const normalizePeers = (basePeers, options) => { + const { peers: sentPeers, skipPeers } = options; + + let peers = sentPeers || basePeers || []; + + if (skipPeers) { + peers = peers.filter((p) => skipPeers.indexOf(p) === -1); + } + + return peers; +}; + +// +// We should create a time to live index in this table to remove fulfilled events +// +class FederationEventsModel extends Base { + constructor() { + super('federation_events'); + } + + // Sometimes events errored but the error is final + setEventAsErrored(e, error, fulfilled = false) { + this.update({ _id: e._id }, { + $set: { + fulfilled, + lastAttemptAt: new Date(), + error, + }, + }); + } + + setEventAsFullfilled(e) { + this.update({ _id: e._id }, { + $set: { fulfilled: true }, + $unset: { error: 1 }, + }); + } + + createEvent(type, payload, peer) { + const record = { + t: type, + ts: new Date(), + fulfilled: false, + payload, + peer, + }; + + record._id = this.insert(record); + + this.emit('createEvent', record); + + return record; + } + + createEventForPeers(type, payload, peers) { + const records = []; + + for (const peer of peers) { + const record = this.createEvent(type, payload, peer); + + records.push(record); + } + + return records; + } + + // Create a `directRoomCreated(drc)` event + directRoomCreated(federatedRoom, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + room: federatedRoom.getRoom(), + owner: federatedRoom.getOwner(), + users: federatedRoom.getUsers(), + }; + + return this.createEventForPeers('drc', payload, peers); + } + + // Create a `roomCreated(roc)` event + roomCreated(federatedRoom, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + room: federatedRoom.getRoom(), + owner: federatedRoom.getOwner(), + users: federatedRoom.getUsers(), + }; + + return this.createEventForPeers('roc', payload, peers); + } + + // Create a `userJoined(usj)` event + userJoined(federatedRoom, federatedUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + user: federatedUser.getUser(), + }; + + return this.createEventForPeers('usj', payload, peers); + } + + // Create a `userAdded(usa)` event + userAdded(federatedRoom, federatedUser, federatedInviter, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_inviter_id: federatedInviter.getFederationId(), + user: federatedUser.getUser(), + }; + + return this.createEventForPeers('usa', payload, peers); + } + + // Create a `userLeft(usl)` event + userLeft(federatedRoom, federatedUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + }; + + return this.createEventForPeers('usl', payload, peers); + } + + // Create a `userRemoved(usr)` event + userRemoved(federatedRoom, federatedUser, federatedRemovedByUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + federated_removed_by_user_id: federatedRemovedByUser.getFederationId(), + }; + + return this.createEventForPeers('usr', payload, peers); + } + + // Create a `userMuted(usm)` event + userMuted(federatedRoom, federatedUser, federatedMutedByUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + federated_muted_by_user_id: federatedMutedByUser.getFederationId(), + }; + + return this.createEventForPeers('usm', payload, peers); + } + + // Create a `userUnmuted(usu)` event + userUnmuted(federatedRoom, federatedUser, federatedUnmutedByUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + federated_unmuted_by_user_id: federatedUnmutedByUser.getFederationId(), + }; + + return this.createEventForPeers('usu', payload, peers); + } + + // Create a `messageCreated(msc)` event + messageCreated(federatedRoom, federatedMessage, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + message: federatedMessage.getMessage(), + }; + + return this.createEventForPeers('msc', payload, peers); + } + + // Create a `messageUpdated(msu)` event + messageUpdated(federatedRoom, federatedMessage, federatedUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + message: federatedMessage.getMessage(), + federated_user_id: federatedUser.getFederationId(), + }; + + return this.createEventForPeers('msu', payload, peers); + } + + // Create a `deleteMessage(msd)` event + messageDeleted(federatedRoom, federatedMessage, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_message_id: federatedMessage.getFederationId(), + }; + + return this.createEventForPeers('msd', payload, peers); + } + + // Create a `messagesRead(msr)` event + messagesRead(federatedRoom, federatedUser, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + }; + + return this.createEventForPeers('msr', payload, peers); + } + + // Create a `messagesSetReaction(mrs)` event + messagesSetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_message_id: federatedMessage.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + reaction, + shouldReact, + }; + + return this.createEventForPeers('mrs', payload, peers); + } + + // Create a `messagesUnsetReaction(mru)` event + messagesUnsetReaction(federatedRoom, federatedMessage, federatedUser, reaction, shouldReact, options = {}) { + const peers = normalizePeers(federatedRoom.getPeers(), options); + + const payload = { + federated_room_id: federatedRoom.getFederationId(), + federated_message_id: federatedMessage.getFederationId(), + federated_user_id: federatedUser.getFederationId(), + reaction, + shouldReact, + }; + + return this.createEventForPeers('mru', payload, peers); + } + + // Get all unfulfilled events + getUnfulfilled() { + return this.find({ fulfilled: false }, { sort: { ts: 1 } }).fetch(); + } + + +} + +export const FederationEvents = new FederationEventsModel(); diff --git a/packages/rocketchat-models/server/models/FederationKeys.js b/packages/rocketchat-models/server/models/FederationKeys.js new file mode 100644 index 00000000000..8e2e9c26756 --- /dev/null +++ b/packages/rocketchat-models/server/models/FederationKeys.js @@ -0,0 +1,69 @@ +import NodeRSA from 'node-rsa'; +import uuid from 'uuid/v4'; + +import { Base } from './_Base'; + +class FederationKeysModel extends Base { + constructor() { + super('federation_keys'); + } + + getKey(type) { + const keyResource = this.findOne({ type }); + + if (!keyResource) { return null; } + + return keyResource.key; + } + + loadKey(keyData, type) { + return new NodeRSA(keyData, `pkcs8-${ type }-pem`); + } + + generateKeys() { + const key = new NodeRSA({ b: 512 }); + + key.generateKeyPair(); + + this.update({ type: 'private' }, { type: 'private', key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, '') }, { upsert: true }); + + this.update({ type: 'public' }, { type: 'public', key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, '') }, { upsert: true }); + + return { + privateKey: this.getPrivateKey(), + publicKey: this.getPublicKey(), + }; + } + + generateUniqueId() { + const uniqueId = uuid(); + + this.update({ type: 'unique' }, { type: 'unique', key: uniqueId }, { upsert: true }); + } + + getUniqueId() { + return (this.findOne({ type: 'unique' }) || {}).key; + } + + getPrivateKey() { + const keyData = this.getKey('private'); + + return keyData && this.loadKey(keyData, 'private'); + } + + getPrivateKeyString() { + return this.getKey('private'); + } + + getPublicKey() { + const keyData = this.getKey('public'); + + return keyData && this.loadKey(keyData, 'public'); + } + + getPublicKeyString() { + return this.getKey('public'); + } +} + +export const FederationKeys = new FederationKeysModel(); diff --git a/packages/rocketchat-models/server/models/Messages.js b/packages/rocketchat-models/server/models/Messages.js index cff4ddd3536..c33eb9532c7 100644 --- a/packages/rocketchat-models/server/models/Messages.js +++ b/packages/rocketchat-models/server/models/Messages.js @@ -813,6 +813,16 @@ export class Messages extends Base { return this.createWithTypeRoomIdMessageAndUser('subscription-role-removed', roomId, message, user, extraData); } + createRejectedMessageByPeer(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('rejected-message-by-peer', roomId, message, user, extraData); + } + + createPeerDoesNotExist(roomId, user, extraData) { + const message = user.username; + return this.createWithTypeRoomIdMessageAndUser('peer-does-not-exist', roomId, message, user, extraData); + } + // REMOVE removeById(_id) { const query = { _id }; diff --git a/packages/rocketchat-models/server/models/Users.js b/packages/rocketchat-models/server/models/Users.js index 1f9377daea2..7b2e368709e 100644 --- a/packages/rocketchat-models/server/models/Users.js +++ b/packages/rocketchat-models/server/models/Users.js @@ -487,7 +487,7 @@ export class Users extends Base { return this.find(query, options); } - findByActiveUsersExcept(searchTerm, exceptions, options) { + findByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields) { if (exceptions == null) { exceptions = []; } if (options == null) { options = {}; } if (!_.isArray(exceptions)) { @@ -496,7 +496,9 @@ export class Users extends Base { const termRegex = new RegExp(s.escapeRegExp(searchTerm), 'i'); - const orStmt = _.reduce(this.settings.get('Accounts_SearchFields').trim().split(','), function(acc, el) { + const searchFields = forcedSearchFields || this.settings.get('Accounts_SearchFields').trim().split(','); + + const orStmt = _.reduce(searchFields, function(acc, el) { acc.push({ [el.trim()]: termRegex }); return acc; }, []); diff --git a/packages/rocketchat-reactions/server/index.js b/packages/rocketchat-reactions/server/index.js index 8c5644023db..e7490bab582 100644 --- a/packages/rocketchat-reactions/server/index.js +++ b/packages/rocketchat-reactions/server/index.js @@ -1 +1 @@ -import './setReaction'; +export { setReaction } from './setReaction'; diff --git a/packages/rocketchat-reactions/server/setReaction.js b/packages/rocketchat-reactions/server/setReaction.js index 25c3104423b..5b5a8c738b8 100644 --- a/packages/rocketchat-reactions/server/setReaction.js +++ b/packages/rocketchat-reactions/server/setReaction.js @@ -16,88 +16,95 @@ const removeUserReaction = (message, reaction, username) => { return message; }; -Meteor.methods({ - setReaction(reaction, messageId, shouldReact) { - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' }); - } - - const message = Messages.findOneById(messageId); +export function setReaction(room, user, message, reaction, shouldReact) { + reaction = `:${ reaction.replace(/:/g, '') }:`; - if (!message) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); - } + if (!emoji.list[reaction] && EmojiCustom.findByNameOrAlias(reaction).count() === 0) { + throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { method: 'setReaction' }); + } - const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); + if (Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !room.reactWhenReadOnly) { + Notifications.notifyUser(Meteor.userId(), 'message', { + _id: Random.id(), + rid: room._id, + ts: new Date(), + msg: TAPi18n.__('You_have_been_muted', {}, user.language), + }); + return false; + } else if (!Subscriptions.findOne({ rid: message.rid })) { + return false; + } - if (!room) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); - } + const userAlreadyReacted = Boolean(message.reactions) && Boolean(message.reactions[reaction]) && message.reactions[reaction].usernames.indexOf(user.username) !== -1; + // When shouldReact was not informed, toggle the reaction. + if (shouldReact === undefined) { + shouldReact = !userAlreadyReacted; + } - reaction = `:${ reaction.replace(/:/g, '') }:`; + if (userAlreadyReacted === shouldReact) { + return; + } + if (userAlreadyReacted) { + removeUserReaction(message, reaction, user.username); - if (!emoji.list[reaction] && EmojiCustom.findByNameOrAlias(reaction).count() === 0) { - throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { method: 'setReaction' }); + if (_.isEmpty(message.reactions)) { + delete message.reactions; + if (isTheLastMessage(room, message)) { + Rooms.unsetReactionsInLastMessage(room._id); + } + Messages.unsetReactions(message._id); + callbacks.run('unsetReaction', message._id, reaction); + callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact }); + } else { + if (isTheLastMessage(room, message)) { + Rooms.setReactionsInLastMessage(room._id, message); + } + Messages.setReactions(message._id, message.reactions); + callbacks.run('setReaction', message._id, reaction); + callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); } + } else { + if (!message.reactions) { + message.reactions = {}; + } + if (!message.reactions[reaction]) { + message.reactions[reaction] = { + usernames: [], + }; + } + message.reactions[reaction].usernames.push(user.username); + if (isTheLastMessage(room, message)) { + Rooms.setReactionsInLastMessage(room._id, message); + } + Messages.setReactions(message._id, message.reactions); + callbacks.run('setReaction', message._id, reaction); + callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); + } + + msgStream.emit(message.rid, message); +} +Meteor.methods({ + setReaction(reaction, messageId, shouldReact) { const user = Meteor.user(); - if (Array.isArray(room.muted) && room.muted.indexOf(user.username) !== -1 && !room.reactWhenReadOnly) { - Notifications.notifyUser(Meteor.userId(), 'message', { - _id: Random.id(), - rid: room._id, - ts: new Date(), - msg: TAPi18n.__('You_have_been_muted', {}, user.language), - }); - return false; - } else if (!Subscriptions.findOne({ rid: message.rid })) { - return false; - } + const message = Messages.findOneById(messageId); - const userAlreadyReacted = Boolean(message.reactions) && Boolean(message.reactions[reaction]) && message.reactions[reaction].usernames.indexOf(user.username) !== -1; - // When shouldReact was not informed, toggle the reaction. - if (shouldReact === undefined) { - shouldReact = !userAlreadyReacted; + const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' }); } - if (userAlreadyReacted === shouldReact) { - return; + if (!message) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } - if (userAlreadyReacted) { - removeUserReaction(message, reaction, user.username); - - if (_.isEmpty(message.reactions)) { - delete message.reactions; - if (isTheLastMessage(room, message)) { - Rooms.unsetReactionsInLastMessage(room._id); - } - Messages.unsetReactions(messageId); - callbacks.run('unsetReaction', messageId, reaction); - } else { - if (isTheLastMessage(room, message)) { - Rooms.setReactionsInLastMessage(room._id, message); - } - Messages.setReactions(messageId, message.reactions); - callbacks.run('setReaction', messageId, reaction); - } - } else { - if (!message.reactions) { - message.reactions = {}; - } - if (!message.reactions[reaction]) { - message.reactions[reaction] = { - usernames: [], - }; - } - message.reactions[reaction].usernames.push(user.username); - if (isTheLastMessage(room, message)) { - Rooms.setReactionsInLastMessage(room._id, message); - } - Messages.setReactions(messageId, message.reactions); - callbacks.run('setReaction', messageId, reaction); + + if (!room) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } - msgStream.emit(message.rid, message); + setReaction(room, user, message, reaction, shouldReact); return; }, diff --git a/packages/rocketchat-slashcommands-invite/server/server.js b/packages/rocketchat-slashcommands-invite/server/server.js index 51ace676175..b922680c8f1 100644 --- a/packages/rocketchat-slashcommands-invite/server/server.js +++ b/packages/rocketchat-slashcommands-invite/server/server.js @@ -17,7 +17,8 @@ function Invite(command, params, item) { if (command !== 'invite' || !Match.test(params, String)) { return; } - const usernames = params.split(/[\s,]/).map((username) => username.replace(/^@/, '')).filter((a) => a !== ''); + + const usernames = params.split(/[\s,]/).map((username) => username.replace(/(^@)|( @)/, '')).filter((a) => a !== ''); if (usernames.length === 0) { return; } diff --git a/packages/rocketchat-ui/client/views/app/directory.html b/packages/rocketchat-ui/client/views/app/directory.html index 7cfc14d05a9..85327e825f6 100644 --- a/packages/rocketchat-ui/client/views/app/directory.html +++ b/packages/rocketchat-ui/client/views/app/directory.html @@ -3,14 +3,25 @@ {{> header sectionName="Directory" hideHelp=true fullpage=true}}

{{>tabs tabs=tabsData}} - {{username}} + {{email}} + {{#if $eq searchWorkspace 'all'}} + {{domain}} + {{/if}} {{createdAt}} {{else}} diff --git a/packages/rocketchat-ui/client/views/app/directory.js b/packages/rocketchat-ui/client/views/app/directory.js index 7cb91a5b3f4..a21f9fd8102 100644 --- a/packages/rocketchat-ui/client/views/app/directory.js +++ b/packages/rocketchat-ui/client/views/app/directory.js @@ -27,7 +27,10 @@ function directorySearch(config, cb) { return { name: result.name, username: result.username, + // If there is no email address (probably only rocket.cat) show the username) + email: (result.emails && result.emails[0] && result.emails[0].address) || result.username, createdAt: timeAgo(result.createdAt, t), + domain: result.federation && result.federation.peer, }; } return null; @@ -36,9 +39,15 @@ function directorySearch(config, cb) { } Template.directory.helpers({ + federationEnabled() { + return settings.get('FEDERATION_Enabled'); + }, searchText() { return Template.instance().searchText.get(); }, + searchWorkspace() { + return Template.instance().searchWorkspace.get(); + }, showLastMessage() { return settings.get('Store_Last_Message'); }, @@ -107,18 +116,35 @@ Template.directory.helpers({ }; }, onTableItemClick() { - const { searchType } = Template.instance(); + const instance = Template.instance(); + + const { searchType } = instance; + let type; let routeConfig; + return function(item) { - if (searchType.get() === 'channels') { - type = 'c'; - routeConfig = { name: item.name }; + // This means we need to add this user locally first + if (item.remoteOnly) { + Meteor.call('federationAddUser', item.email, item.domain, (error, federatedUser) => { + if (!federatedUser) { return; } + + // Reload + instance.end.set(false); + // directorySearch.call(instance); + + roomTypes.openRouteLink('d', { name: item.username }); + }); } else { - type = 'd'; - routeConfig = { name: item.username }; + if (searchType.get() === 'channels') { + type = 'c'; + routeConfig = { name: item.name }; + } else { + type = 'd'; + routeConfig = { name: item.username }; + } + roomTypes.openRouteLink(type, routeConfig); } - roomTypes.openRouteLink(type, routeConfig); }; }, isLoading() { @@ -170,36 +196,84 @@ Template.directory.events({ t.page.set(0); t.searchText.set(e.currentTarget.value); }, 300), + 'change .js-workspace': (e, t) => { + t.end.set(false); + t.sortDirection.set('asc'); + t.page.set(0); + t.searchWorkspace.set(e.target.value); + }, }); Template.directory.onRendered(function() { + function setResults(result) { + if (!Array.isArray(result)) { + result = []; + } + + if (this.page.get() > 0) { + return this.results.set([...this.results.get(), ...result]); + } + + return this.results.set(result); + } + Tracker.autorun(() => { + const searchConfig = { text: this.searchText.get(), + workspace: this.searchWorkspace.get(), type: this.searchType.get(), sortBy: this.searchSortBy.get(), sortDirection: this.sortDirection.get(), limit: this.limit.get(), page: this.page.get(), }; + if (this.end.get() || this.loading) { return; } + this.loading = true; this.isLoading.set(true); + directorySearch(searchConfig, (result) => { this.loading = false; this.isLoading.set(false); this.end.set(!result); - if (!Array.isArray(result)) { - result = []; - } + // If there is no result, searching every workspace and + // the search text is an email address, try to find a federated user + if (this.searchWorkspace.get() === 'all' && this.searchText.get().indexOf('@') !== -1) { + const email = this.searchText.get(); + + Meteor.call('federationSearchUsers', email, (error, federatedUsers) => { + if (!federatedUsers) { return; } + + result = result || []; + + for (const federatedUser of federatedUsers) { + const { user } = federatedUser; - if (this.page.get() > 0) { - return this.results.set([...this.results.get(), ...result]); + const exists = result.findIndex((e) => e.domain === user.federation.peer && e.username === user.username) !== -1; + + if (exists) { continue; } + + // Add the federated user to the results + result.unshift({ + remoteOnly: true, + name: user.name, + username: user.username, + email: user.emails && user.emails[0] && user.emails[0].address, + createdAt: timeAgo(user.createdAt, t), + domain: user.federation.peer, + }); + } + + setResults.call(this, result); + }); } - return this.results.set(result); + + setResults.call(this, result); }); }); }); @@ -215,6 +289,7 @@ Template.directory.onCreated(function() { this.sortDirection = new ReactiveVar('asc'); } this.searchText = new ReactiveVar(''); + this.searchWorkspace = new ReactiveVar('local'); this.limit = new ReactiveVar(0); this.page = new ReactiveVar(0); this.end = new ReactiveVar(false); diff --git a/packages/rocketchat-utils/lib/getValidRoomName.js b/packages/rocketchat-utils/lib/getValidRoomName.js index de891909247..8d726b21927 100644 --- a/packages/rocketchat-utils/lib/getValidRoomName.js +++ b/packages/rocketchat-utils/lib/getValidRoomName.js @@ -3,7 +3,7 @@ import limax from 'limax'; import { settings } from 'meteor/rocketchat:settings'; import { Rooms } from 'meteor/rocketchat:models'; -export const getValidRoomName = (displayName, rid = '') => { +export const getValidRoomName = (displayName, rid = '', options = {}) => { let slugifiedName = displayName; if (settings.get('UI_Allow_room_names_with_special_chars')) { @@ -19,11 +19,17 @@ export const getValidRoomName = (displayName, rid = '') => { } let nameValidation; - try { - nameValidation = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`); - } catch (error) { - nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + + if (options.nameValidationRegex) { + nameValidation = new RegExp(options.nameValidationRegex); + } else { + try { + nameValidation = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`); + } catch (error) { + nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); + } } + if (!nameValidation.test(slugifiedName)) { throw new Meteor.Error('error-invalid-room-name', `${ slugifiedName } is not a valid room name.`, { function: 'RocketChat.getValidRoomName', diff --git a/packages/rocketchat_theme/client/imports/components/modal/directory.css b/packages/rocketchat_theme/client/imports/components/modal/directory.css index 013ddaf4aea..45926620481 100644 --- a/packages/rocketchat_theme/client/imports/components/modal/directory.css +++ b/packages/rocketchat_theme/client/imports/components/modal/directory.css @@ -6,8 +6,27 @@ padding: 1.25rem 2rem; + &-fields { + display: flex; + flex-direction: row; + + height: 89px; + + margin-right: -5px; + margin-left: -5px; + + padding: 10px 0; + } + + &-dropdown { + height: 48px; + } + &-search { - width: 100%; + height: 48px; + + margin-right: 5px; + margin-left: 5px; & .rc-icon { width: 0.875rem; diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js index a0115689797..4d005190ad8 100644 --- a/server/methods/browseChannels.js +++ b/server/methods/browseChannels.js @@ -4,6 +4,8 @@ import { hasPermission } from 'meteor/rocketchat:authorization'; import { Rooms, Users } from 'meteor/rocketchat:models'; import s from 'underscore.string'; +import { Federation } from 'meteor/rocketchat:federation'; + const sortChannels = function(field, direction) { switch (field) { case 'createdAt': @@ -27,7 +29,7 @@ const sortUsers = function(field, direction) { }; Meteor.methods({ - browseChannels({ text = '', type = 'channels', sortBy = 'name', sortDirection = 'asc', page, offset, limit = 10 }) { + browseChannels({ text = '', workspace = '', type = 'channels', sortBy = 'name', sortDirection = 'asc', page, offset, limit = 10 }) { const regex = new RegExp(s.trim(s.escapeRegExp(text)), 'i'); if (!['channels', 'users'].includes(type)) { @@ -56,27 +58,30 @@ Meteor.methods({ }; const user = Meteor.user(); - if (type === 'channels') { + if (type === 'channels') { const sort = sortChannels(sortBy, sortDirection); if (!hasPermission(user._id, 'view-c-room')) { return; } + + const result = Rooms.findByNameAndType(regex, 'c', { + ...options, + sort, + fields: { + description: 1, + topic: 1, + name: 1, + lastMessage: 1, + ts: 1, + archived: 1, + usersCount: 1, + }, + }); + return { - results: Rooms.findByNameAndType(regex, 'c', { - ...options, - sort, - fields: { - description: 1, - topic: 1, - name: 1, - lastMessage: 1, - ts: 1, - archived: 1, - usersCount: 1, - }, - }).fetch(), - total: Rooms.findByNameAndType(regex, 'c').count(), + total: result.count(), // count ignores the `skip` and `limit` options + results: result.fetch(), }; } @@ -84,19 +89,49 @@ Meteor.methods({ if (!hasPermission(user._id, 'view-outside-room') || !hasPermission(user._id, 'view-d-room')) { return; } + + let exceptions = [user.username]; + + // Get exceptions + if (type === 'users' && workspace === 'all') { + const nonFederatedUsers = Users.find({ + $or: [ + { federation: { $exists: false } }, + { 'federation.peer': Federation.localIdentifier }, + ], + }, { fields: { username: 1 } }).map((u) => u.username); + + exceptions = exceptions.concat(nonFederatedUsers); + } else if (type === 'users' && workspace === 'local') { + const federatedUsers = Users.find({ + $and: [ + { federation: { $exists: true } }, + { 'federation.peer': { $ne: Federation.localIdentifier } }, + ], + }, { fields: { username: 1 } }).map((u) => u.username); + + exceptions = exceptions.concat(federatedUsers); + } + const sort = sortUsers(sortBy, sortDirection); + + const forcedSearchFields = workspace === 'all' && ['username', 'name', 'emails.address']; + + const result = Users.findByActiveUsersExcept(text, exceptions, { + ...options, + sort, + fields: { + username: 1, + name: 1, + createdAt: 1, + emails: 1, + federation: 1, + }, + }, forcedSearchFields); + return { - results: Users.findByActiveUsersExcept(text, [user.username], { - ...options, - sort, - fields: { - username: 1, - name: 1, - createdAt: 1, - emails: 1, - }, - }).fetch(), - total: Users.findByActiveUsersExcept(text, [user.username]).count(), + total: result.count(), // count ignores the `skip` and `limit` options + results: result.fetch(), }; }, }); diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js index b25524bd126..20641a97279 100644 --- a/server/methods/createDirectMessage.js +++ b/server/methods/createDirectMessage.js @@ -5,6 +5,7 @@ import { hasPermission } from 'meteor/rocketchat:authorization'; import { Users, Rooms, Subscriptions } from 'meteor/rocketchat:models'; import { getDefaultSubscriptionPref } from 'meteor/rocketchat:utils'; import { RateLimiter } from 'meteor/rocketchat:lib'; +import { callbacks } from 'meteor/rocketchat:callbacks'; Meteor.methods({ createDirectMessage(username) { @@ -55,7 +56,7 @@ Meteor.methods({ const now = new Date(); // Make sure we have a room - Rooms.upsert({ + const roomUpsertResult = Rooms.upsert({ _id: rid, }, { $set: { @@ -129,6 +130,13 @@ Meteor.methods({ }, }); + // If the room is new, run a callback + if (roomUpsertResult.insertedId) { + const insertedRoom = Rooms.findOneById(rid); + + callbacks.run('afterCreateDirectRoom', insertedRoom, { from: me, to }); + } + return { rid, }; diff --git a/server/methods/muteUserInRoom.js b/server/methods/muteUserInRoom.js index 496ea2b90aa..7605107d573 100644 --- a/server/methods/muteUserInRoom.js +++ b/server/methods/muteUserInRoom.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Rooms, Subscriptions, Users, Messages } from 'meteor/rocketchat:models'; import { hasPermission } from 'meteor/rocketchat:authorization'; +import { callbacks } from 'meteor/rocketchat:callbacks'; Meteor.methods({ muteUserInRoom(data) { @@ -48,10 +49,12 @@ Meteor.methods({ const mutedUser = Users.findOneByUsername(data.username); - Rooms.muteUsernameByRoomId(data.rid, mutedUser.username); - const fromUser = Users.findOneById(fromId); + callbacks.run('beforeMuteUser', { mutedUser, fromUser }, room); + + Rooms.muteUsernameByRoomId(data.rid, mutedUser.username); + Messages.createUserMutedWithRoomIdAndUser(data.rid, mutedUser, { u: { _id: fromUser._id, @@ -59,6 +62,10 @@ Meteor.methods({ }, }); + Meteor.defer(function() { + callbacks.run('afterMuteUser', { mutedUser, fromUser }, room); + }); + return true; }, }); diff --git a/server/methods/readMessages.js b/server/methods/readMessages.js index 789bc40789c..25e98caae00 100644 --- a/server/methods/readMessages.js +++ b/server/methods/readMessages.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { ReadReceipt } from '../../imports/message-read-receipt/server/lib/ReadReceipt'; +import { callbacks } from 'meteor/rocketchat:callbacks'; import { Subscriptions } from 'meteor/rocketchat:models'; Meteor.methods({ @@ -15,13 +15,14 @@ Meteor.methods({ }); } - // this prevents cache from updating object reference/pointer - const userSubscription = Object.assign({}, Subscriptions.findOneByRoomIdAndUserId(rid, userId)); + callbacks.run('beforeReadMessages', rid, userId); + // TODO: move this calls to an exported function + const userSubscription = Subscriptions.findOneByRoomIdAndUserId(rid, userId, { fields: { ls: 1 } }); Subscriptions.setAsReadByRoomIdAndUserId(rid, userId); Meteor.defer(() => { - ReadReceipt.markMessagesAsRead(rid, userId, userSubscription.ls); + callbacks.run('afterReadMessages', rid, { userId, lastSeen: userSubscription.ls }); }); }, }); diff --git a/server/methods/removeUserFromRoom.js b/server/methods/removeUserFromRoom.js index 75e6a79cbf8..38afc512254 100644 --- a/server/methods/removeUserFromRoom.js +++ b/server/methods/removeUserFromRoom.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasPermission, hasRole, getUsersInRole, removeUserFromRoles } from 'meteor/rocketchat:authorization'; import { Users, Subscriptions, Rooms, Messages } from 'meteor/rocketchat:models'; +import { callbacks } from 'meteor/rocketchat:callbacks'; Meteor.methods({ removeUserFromRoom(data) { @@ -34,6 +35,8 @@ Meteor.methods({ const removedUser = Users.findOneByUsername(data.username); + const fromUser = Users.findOneById(fromId); + const subscription = Subscriptions.findOneByRoomIdAndUserId(data.rid, removedUser._id, { fields: { _id: 1 } }); if (!subscription) { throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', { @@ -51,14 +54,14 @@ Meteor.methods({ } } + callbacks.run('beforeRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room); + Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id); if (['c', 'p'].includes(room.t) === true) { removeUserFromRoles(removedUser._id, ['moderator', 'owner'], data.rid); } - const fromUser = Users.findOneById(fromId); - Messages.createUserRemovedWithRoomIdAndUser(data.rid, removedUser, { u: { _id: fromUser._id, @@ -66,6 +69,10 @@ Meteor.methods({ }, }); + Meteor.defer(function() { + callbacks.run('afterRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room); + }); + return true; }, }); diff --git a/server/methods/unmuteUserInRoom.js b/server/methods/unmuteUserInRoom.js index dffe6ee9e86..75c22f77ffc 100644 --- a/server/methods/unmuteUserInRoom.js +++ b/server/methods/unmuteUserInRoom.js @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasPermission } from 'meteor/rocketchat:authorization'; import { Users, Subscriptions, Rooms, Messages } from 'meteor/rocketchat:models'; +import { callbacks } from 'meteor/rocketchat:callbacks'; Meteor.methods({ unmuteUserInRoom(data) { @@ -42,10 +43,12 @@ Meteor.methods({ const unmutedUser = Users.findOneByUsername(data.username); - Rooms.unmuteUsernameByRoomId(data.rid, unmutedUser.username); - const fromUser = Users.findOneById(fromId); + callbacks.run('beforeUnmuteUser', { unmutedUser, fromUser }, room); + + Rooms.unmuteUsernameByRoomId(data.rid, unmutedUser.username); + Messages.createUserUnmutedWithRoomIdAndUser(data.rid, unmutedUser, { u: { _id: fromUser._id, @@ -53,6 +56,10 @@ Meteor.methods({ }, }); + Meteor.defer(function() { + callbacks.run('afterUnmuteUser', { unmutedUser, fromUser }, room); + }); + return true; }, });