From 08bb8a4a191443d09946d559308874c72fdb579a Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 1 Mar 2021 15:39:55 +0000 Subject: [PATCH 01/21] Park changes for now --- package-lock.json | 312 ++++++++++++++++++++++++++++-- package.json | 1 + src/bridge.ts | 42 +++- src/components/app-service-bot.ts | 6 +- src/components/intent.ts | 199 ++++++++++++------- 5 files changed, 464 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bc61dbc..eaebf384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -798,6 +798,11 @@ "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -1104,6 +1109,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, "default-require-extensions": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", @@ -1146,6 +1156,59 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", + "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "entities": "^2.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", + "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", + "requires": { + "domelementtype": "^2.1.0" + } + } + } + }, + "domelementtype": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", + "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" + }, + "domhandler": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", + "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", + "requires": { + "domelementtype": "^2.0.1" + } + }, + "domutils": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz", + "integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0" + }, + "dependencies": { + "domhandler": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", + "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", + "requires": { + "domelementtype": "^2.1.0" + } + } + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -1185,6 +1248,11 @@ "ansi-colors": "^4.1.1" } }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, "es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -1199,8 +1267,7 @@ "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=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "7.17.0", @@ -1725,6 +1792,11 @@ "is-glob": "^4.0.1" } }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -1751,8 +1823,7 @@ "graceful-fs": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.1.tgz", - "integrity": "sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==", - "dev": true + "integrity": "sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw==" }, "handlebars": { "version": "4.7.6", @@ -1794,6 +1865,15 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "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" + } + }, "hasha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", @@ -1804,12 +1884,52 @@ "type-fest": "^0.8.0" } }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html-to-text": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-6.0.0.tgz", + "integrity": "sha512-r0KNC5aqCAItsjlgtirW6RW25c92Ee3ybQj8z//4Sl4suE3HIPqM4deGpYCUJULLjtVPEP1+Ma+1ZeX1iMsCiA==", + "requires": { + "deepmerge": "^4.2.2", + "he": "^1.2.0", + "htmlparser2": "^4.1.0", + "lodash": "^4.17.20", + "minimist": "^1.2.5" + }, + "dependencies": { + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + } + } + }, + "htmlencode": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz", + "integrity": "sha1-9+LWr74YqHp45jujMI51N2Z0Dj8=" + }, + "htmlparser2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", + "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^3.0.0", + "domutils": "^2.0.0", + "entities": "^2.0.0" + } + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -1948,6 +2068,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -2240,8 +2365,7 @@ "lodash": { "version": "4.17.19", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash.flattendeep": { "version": "4.4.0", @@ -2266,11 +2390,22 @@ "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==" }, + "lowdb": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", + "integrity": "sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ==", + "requires": { + "graceful-fs": "^4.1.3", + "is-promise": "^2.1.0", + "lodash": "4", + "pify": "^3.0.0", + "steno": "^0.4.1" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -2335,6 +2470,34 @@ } } }, + "matrix-bot-sdk": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/matrix-bot-sdk/-/matrix-bot-sdk-0.5.13.tgz", + "integrity": "sha512-tWNZbCQC+X5qgVirzEZf72UAV6UQ1UzsZboSB+ZeW1fhEIjomS0AZvon7wsiUTVtuiL2w1QOy0GZSrWiLV8zZw==", + "requires": { + "@types/express": "^4.17.7", + "chalk": "^4.1.0", + "express": "^4.17.1", + "glob-to-regexp": "^0.4.1", + "hash.js": "^1.1.7", + "html-to-text": "^6.0.0", + "htmlencode": "^0.0.4", + "lowdb": "^1.0.0", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "morgan": "^1.10.0", + "request": "^2.88.2", + "request-promise": "^4.2.6", + "sanitize-html": "^1.27.2" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "matrix-js-sdk": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-9.5.0.tgz", @@ -2407,6 +2570,11 @@ "mime-db": "1.44.0" } }, + "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==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2419,8 +2587,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "mkdirp": { "version": "0.5.5", @@ -2733,6 +2900,11 @@ "callsites": "^3.0.0" } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2784,6 +2956,11 @@ "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", "dev": true }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -2793,6 +2970,77 @@ "find-up": "^4.0.0" } }, + "postcss": { + "version": "7.0.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", + "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2940,6 +3188,25 @@ } } }, + "request-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", + "requires": { + "bluebird": "^3.5.0", + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + } + }, + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "requires": { + "lodash": "^4.17.19" + } + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3004,6 +3271,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-html": { + "version": "1.27.5", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz", + "integrity": "sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A==", + "requires": { + "htmlparser2": "^4.1.0", + "lodash": "^4.17.15", + "parse-srcset": "^1.0.2", + "postcss": "^7.0.27" + } + }, "semver": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", @@ -3227,6 +3505,19 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "steno": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/steno/-/steno-0.4.4.tgz", + "integrity": "sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs=", + "requires": { + "graceful-fs": "^4.1.3" + } + }, "string-width": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", @@ -3724,8 +4015,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "15.4.1", diff --git a/package.json b/package.json index b1187856..36dc050d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "is-my-json-valid": "^2.20.5", "js-yaml": "^4.0.0", "matrix-appservice": "^0.7.1", + "matrix-bot-sdk": "^0.5.13", "matrix-js-sdk": "^9.5.0", "nedb": "^1.8.0", "nopt": "^5.0.0", diff --git a/src/bridge.ts b/src/bridge.ts index 09865a17..91591974 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -51,6 +51,7 @@ import { RemoteRoom } from "./models/rooms/remote"; import { Registry } from "prom-client"; import { ClientEncryptionStore, EncryptedEventBroker } from "./components/encryption"; import { EphemeralEvent, PresenceEvent, ReadReceiptEvent, TypingEvent, WeakEvent } from "./components/event-types"; +import BotSDK from "matrix-bot-sdk"; const log = logging.get("bridge"); @@ -407,6 +408,7 @@ export class Bridge { private eventStore?: EventBridgeStore; private registration?: AppServiceRegistration; private appservice?: AppService; + private botSdkAS?: BotSDK.Appservice; private eeEventBroker?: EncryptedEventBroker; private selfPingDeferred?: { defer: Defer; @@ -562,6 +564,24 @@ export class Bridge { if (!asToken) { throw Error('No AS token provided, cannot create ClientFactory'); } + const rawReg = this.registration.getOutput(); + this.botSdkAS = new BotSDK.Appservice({ + registration: { + ...rawReg, + url: rawReg.url || undefined, + protocols: rawReg.protocols || undefined, + namespaces: { + users: rawReg.namespaces?.users || [], + rooms: rawReg.namespaces?.rooms || [], + aliases: rawReg.namespaces?.aliases || [], + } + }, + homeserverUrl: this.opts.homeserverUrl, + homeserverName: this.opts.domain, + // Unused atm. + port: 0, + bindAddress: "127.0.0.1", + }); this.clientFactory = this.opts.clientFactory || new ClientFactory({ url: this.opts.homeserverUrl, @@ -1022,20 +1042,20 @@ export class Bridge { * @return The intent instance */ public getIntent(userId?: string, request?: Request) { - if (!this.clientFactory) { - throw Error('Cannot call getIntent before calling .run()'); + if (!this.appServiceBot) { + throw Error('Cannot call getIntent before calling .initalise()'); } if (!userId) { if (!this.botIntent) { // This will be defined when .run is called. - throw Error('Cannot call getIntent before calling .run()'); + throw Error('Cannot call getIntent before calling .initalise()'); } return this.botIntent; } else if (userId === this.botUserId) { if (!this.botIntent) { // This will be defined when .run is called. - throw Error('Cannot call getIntent before calling .run()'); + throw Error('Cannot call getIntent before calling .initalise()'); } return this.botIntent; } @@ -1051,14 +1071,16 @@ export class Bridge { return existingIntent.intent; } - const client = this.clientFactory.getClientAs( - userId, - request, - this.opts.bridgeEncryption?.homeserverUrl, - !!this.opts.bridgeEncryption, - ); const clientIntentOpts: IntentOpts = { backingStore: this.intentBackingStore, + getJsSdkClient: () => + this.clientFactory.getClientAs( + userId, + request, + this.opts.bridgeEncryption?.homeserverUrl, + !!this.opts.bridgeEncryption, + ), + ...this.opts.intentOptions?.clients, }; clientIntentOpts.registered = this.membershipCache.isUserRegistered(userId); diff --git a/src/components/app-service-bot.ts b/src/components/app-service-bot.ts index 44439284..0e34a1ab 100644 --- a/src/components/app-service-bot.ts +++ b/src/components/app-service-bot.ts @@ -17,6 +17,7 @@ limitations under the License. import { AppServiceRegistration } from "matrix-appservice"; import { MembershipCache, UserProfile } from "./membership-cache"; import { StateLookupEvent } from ".."; +import { MatrixClient } from "matrix-bot-sdk"; /** * Construct an AS bot user which has various helper methods. @@ -29,7 +30,8 @@ import { StateLookupEvent } from ".."; */ export class AppServiceBot { private exclusiveUserRegexes: RegExp[]; - constructor (private client: any, registration: AppServiceRegistration, private memberCache: MembershipCache) { + constructor (private client: MatrixClient, private userId: string, registration: AppServiceRegistration, + private memberCache: MembershipCache) { // yank out the exclusive user ID regex strings this.exclusiveUserRegexes = []; const regOut = registration.getOutput(); @@ -48,7 +50,7 @@ export class AppServiceBot { } public getUserId(): string { - return this.client.credentials.userId; + return this.userId; } /** diff --git a/src/components/intent.ts b/src/components/intent.ts index 4dce2bd9..bce49b10 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ /* Copyright 2020 The Matrix.org Foundation C.I.C. @@ -26,15 +27,16 @@ import BridgeErrorReason = unstable.BridgeErrorReason; import { APPSERVICE_LOGIN_TYPE, ClientEncryptionSession } from "./encryption"; import Logging from "./logging"; import { ReadStream } from "fs"; +import BotSdk from "matrix-bot-sdk"; const log = Logging.get("Intent"); export type IntentBackingStore = { getMembership: (roomId: string, userId: string) => UserMembership, getMemberProfile: (roomId: string, userid: string) => UserProfile, - getPowerLevelContent: (roomId: string) => Record | undefined, + getPowerLevelContent: (roomId: string) => PowerLevelContent | undefined, setMembership: (roomId: string, userId: string, membership: UserMembership, profile: UserProfile) => void, - setPowerLevelContent: (roomId: string, content: Record) => void, + setPowerLevelContent: (roomId: string, content: PowerLevelContent) => void, }; export interface IntentOpts { @@ -47,6 +49,8 @@ export interface IntentOpts { dontJoin?: boolean; enablePresence?: boolean; registered?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getJsSdkClient?: () => any, encryption?: { sessionPromise: Promise; sessionCreatedCallback: (session: ClientEncryptionSession) => Promise; @@ -109,6 +113,8 @@ export class Intent { ttl: number, size: number, }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getJsSdkClient?: () => any, dontCheckPowerLevel?: boolean; dontJoin?: boolean; enablePresence: boolean; @@ -124,11 +130,15 @@ export class Intent { ensureClientSyncingCallback: () => Promise; }; private readyPromise?: Promise; + // The legacyClient is created on demand when bridges need to use + // it, but is not created by default anymore. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private legacyClient?: any; /** * Create an entity which can fulfil the intent of a given user. * @constructor - * @param client The matrix client instance whose intent is being + * @param client The bot sdk intent which this intent wraps * fulfilled e.g. the entity joining the room when you call intent.join(roomId). * @param botClient The client instance for the AS bot itself. * This will be used to perform more priveleged actions such as creating new @@ -170,7 +180,7 @@ export class Intent { * @param opts.caching.ttl How long requests can stay in the cache, in milliseconds. * @param opts.caching.size How many entries should be kept in the cache, before the oldest is dropped. */ - constructor(public readonly client: any, private readonly botClient: any, opts: IntentOpts = {}) { + constructor(public readonly botSdkIntent: BotSdk.Intent, private readonly botClient: any, opts: IntentOpts = {}) { if (opts.backingStore) { if (!opts.backingStore.setPowerLevelContent || !opts.backingStore.getPowerLevelContent || @@ -190,7 +200,7 @@ export class Intent { return this._membershipStates[roomId] && this._membershipStates[roomId][0]; }, getMemberProfile: (roomId: string, userId: string) => { - if (userId !== this.client.credentials.userId) { + if (userId !== this.userId) { return {}; } return this._membershipStates[roomId] && this._membershipStates[roomId][1]; @@ -204,7 +214,7 @@ export class Intent { } this._membershipStates[roomId] = [membership, profile]; }, - setPowerLevelContent: (roomId: string, content: Record) => { + setPowerLevelContent: (roomId: string, content: PowerLevelContent) => { this._powerLevels[roomId] = content; }, }, @@ -240,15 +250,38 @@ export class Intent { } /** - * Return the client this Intent is acting on behalf of. + * Legacy property to access the matrix-js-sdk. + * @deprecated Support for the matrix-js-sdk client will be going away in + * a future release. Where possible, the intent object functions should be + * used. The `botSdkIntent` also provides access to the new client. + * @see getClient + */ + public get client() { + return this.getClient(); + } + + /** + * Return a matrix-js-sdk client, which is created on demand. + * @deprecated Support for the matrix-js-sdk client will be going away in + * a future release. Where possible, the intent object functions should be + * used. The `botSdkIntent` also provides access to the new client. * @return The client */ public getClient() { - return this.client; + if (this.legacyClient) { + return this.legacyClient; + } + if (!this.opts.getJsSdkClient) { + throw Error('Legacy client not available'); + } + log.warn("Support for the matrix-js-sdk will be going away in a future release." + + "Please update occurances of Intent.getClient() and Intent.client"); + this.legacyClient = this.opts.getJsSdkClient(); + return this.legacyClient; } public get userId(): string { - return this.client.credentials.userId; + return this.botSdkIntent.userId; } /** @@ -257,25 +290,20 @@ export class Intent { * @throws If the provided string was incorrectly formatted or alias does not exist. */ public async resolveRoom(roomAliasOrId: string): Promise { - if (roomAliasOrId.startsWith("!")) { - return roomAliasOrId; - } - else if (roomAliasOrId.startsWith("#")) { - const r = await this.client.resolveRoomAlias(roomAliasOrId); - return r.room_id; - } - throw Error('Invalid roomId/roomAlias provided'); + return this.botSdkIntent.underlyingClient.resolveRoom(roomAliasOrId); } /** - *

Send a plaintext message to a room.

+ * Send a plaintext message to a room. + * * This will automatically make the client join the room so they can send the * message if they are not already joined. It will also make sure that the client * has sufficient power level to do this. * @param roomId The room to send to. * @param text The text string to send. + * @returns The Matrix event ID. */ - public sendText(roomId: string, text: string) { + public sendText(roomId: string, text: string): Promise<{event_id: string}> { return this.sendMessage(roomId, { body: text, msgtype: "m.text" @@ -283,35 +311,41 @@ export class Intent { } /** - *

Set the name of a room.

+ * Set the name of a room. + * * This will automatically make the client join the room so they can set the * name if they are not already joined. It will also make sure that the client * has sufficient power level to do this. * @param roomId The room to send to. * @param name The room name. + * @returns The Matrix event ID. */ - public setRoomName(roomId: string, name: string) { - return this.sendStateEvent(roomId, "m.room.name", "", { + public async setRoomName(roomId: string, name: string): Promise<{event_id: string}> { + const eventId = await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, "m.room.name", "", { name: name }); + return {event_id: eventId}; } /** - *

Set the topic of a room.

+ * Set the topic of a room. + * * This will automatically make the client join the room so they can set the * topic if they are not already joined. It will also make sure that the client * has sufficient power level to do this. * @param roomId The room to send to. * @param topic The room topic. */ - public setRoomTopic(roomId: string, topic: string) { - return this.sendStateEvent(roomId, "m.room.topic", "", { + public async setRoomTopic(roomId: string, topic: string): Promise<{event_id: string}> { + const eventId = await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, "m.room.topic", "", { topic: topic }); + return {event_id: eventId}; } /** - *

Set the avatar of a room.

+ * Set the avatar of a room. + * * This will automatically make the client join the room so they can set the * topic if they are not already joined. It will also make sure that the client * has sufficient power level to do this. @@ -319,7 +353,7 @@ export class Intent { * @param avatar The url of the avatar. * @param info Extra information about the image. See m.room.avatar for details. */ - public setRoomAvatar(roomId: string, avatar: string, info?: string) { + public setRoomAvatar(roomId: string, avatar: string, info?: string): Promise<{event_id: string}> { const content = { info, url: avatar, @@ -328,7 +362,8 @@ export class Intent { } /** - *

Send a typing event to a room.

+ * Send a typing event to a room. + * * This will automatically make the client join the room so they can send the * typing event if they are not already joined. * @param roomId The room to send to. @@ -336,23 +371,20 @@ export class Intent { */ public async sendTyping(roomId: string, isTyping: boolean) { await this._ensureJoined(roomId); - return this.client.sendTyping(roomId, isTyping); + return this.botSdkIntent.underlyingClient.setTyping(roomId, isTyping); } /** - *

Send a read receipt to a room.

+ * Send a read receipt to a room. + * * This will automatically make the client join the room so they can send the * receipt event if they are not already joined. * @param roomId The room to send to. * @param eventId The event ID to set the receipt mark to. */ public async sendReadReceipt(roomId: string, eventId: string) { - const event = new MatrixEvent({ - room_id: roomId, - event_id: eventId, - }); await this._ensureJoined(roomId); - return this.client.sendReadReceipt(event); + return this.botSdkIntent.underlyingClient.sendReadReceipt(roomId, eventId); } /** @@ -363,12 +395,19 @@ export class Intent { */ public async setPowerLevel(roomId: string, target: string, level: number|undefined) { await this._ensureJoined(roomId); - const event = await this._ensureHasPowerLevelFor(roomId, "m.room.power_levels", true); - return this.client.setPowerLevel(roomId, target, level, event); + const plContent = this.opts.backingStore.getPowerLevelContent(roomId); + if (plContent && plContent?.users) { + plContent.users[target] = level; + } + else if (level !== undefined) { + plContent.users = { [target]: level}; + } + await this.botSdkIntent.underlyingClient.setUserPowerLevel(roomId, target, level); } /** - *

Send an m.room.message event to a room.

+ * Send an `m.room.message` event to a room. + * * This will automatically make the client join the room so they can send the * message if they are not already joined. It will also make sure that the client * has sufficient power level to do this. @@ -380,7 +419,8 @@ export class Intent { } /** - *

Send a message event to a room.

+ * Send a message event to a room. + * * This will automatically make the client join the room so they can send the * message if they are not already joined. It will also make sure that the client * has sufficient power level to do this. @@ -409,14 +449,17 @@ export class Intent { } await this._ensureJoined(roomId); await this._ensureHasPowerLevelFor(roomId, type, false); - return this._joinGuard(roomId, async() => - // eslint-disable-next-line camelcase - this.client.sendEvent(roomId, type, content) as Promise<{event_id: string}> - ); + return this._joinGuard(roomId, async() => { + return { + // eslint-disable-next-line camelcase + event_id: await this.botSdkIntent.underlyingClient.sendEvent(roomId, type, content), + } + }); } /** - *

Send a state event to a room.

+ * Send a state event to a room. + * * This will automatically make the client join the room so they can send the * state if they are not already joined. It will also make sure that the client * has sufficient power level to do this. @@ -430,14 +473,17 @@ export class Intent { ): Promise<{event_id: string}> { await this._ensureJoined(roomId); await this._ensureHasPowerLevelFor(roomId, type, true); - return this._joinGuard(roomId, async() => - // eslint-disable-next-line camelcase - this.client.sendStateEvent(roomId, type, content, skey) as Promise<{event_id: string}> - ); + return this._joinGuard(roomId, async() => { + return { + // eslint-disable-next-line camelcase + event_id: await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, type, skey, content) + } + }); } /** - *

Get the current room state for a room.

+ * Get the current room state for a room. + * * This will automatically make the client join the room so they can get the * state if they are not already joined. * @param roomId The room to get the state from. @@ -449,7 +495,7 @@ export class Intent { if (useCache) { return this._requestCaches.roomstate.get(roomId); } - return this.client.roomState(roomId); + return this.botSdkIntent.underlyingClient.getRoomState(roomId); } /** @@ -507,7 +553,8 @@ export class Intent { } /** - *

Invite a user to a room.

+ * Invite a user to a room. + * * This will automatically make the client join the room so they can send the * invite if they are not already joined. * @param roomId The room to invite the user to. @@ -520,7 +567,8 @@ export class Intent { } /** - *

Kick a user from a room.

+ * Kick a user from a room. + * * This will automatically make the client join the room so they can send the * kick if they are not already joined. * @param roomId The room to kick the user from. @@ -537,7 +585,8 @@ export class Intent { } /** - *

Ban a user from a room.

+ * Ban a user from a room. + * * This will automatically make the client join the room so they can send the * ban if they are not already joined. * @param roomId The room to ban the user from. @@ -551,7 +600,8 @@ export class Intent { } /** - *

Unban a user from a room.

+ * Unban a user from a room. + * * This will automatically make the client join the room so they can send the * unban if they are not already joined. * @param roomId The room to unban the user from. @@ -564,7 +614,8 @@ export class Intent { } /** - *

Join a room

+ * Join a room + * * This will automatically send an invite from the bot if it is an invite-only * room, which may make the bot attempt to join the room if it isn't already. * @param roomIdOrAlias The room ID or room alias to join. @@ -576,7 +627,8 @@ export class Intent { } /** - *

Leave a room

+ * Leave a room + * * This will no-op if the user isn't in the room. * @param roomId The room to leave. * @param reason An optional string to explain why the user left the room. @@ -589,7 +641,8 @@ export class Intent { } /** - *

Get a user's profile information

+ * Get a user's profile information + * * @param userId The ID of the user whose profile to return * @param info The profile field name to retrieve (e.g. 'displayname' * or 'avatar_url'), or null to fetch the entire profile information. @@ -607,7 +660,8 @@ export class Intent { } /** - *

Set the user's display name

+ * Set the user's display name + * * @param name The new display name */ public async setDisplayName(name: string) { @@ -616,7 +670,8 @@ export class Intent { } /** - *

Set the user's avatar URL

+ * Set the user's avatar URL + * * @param url The new avatar URL */ public async setAvatarUrl(url: string) { @@ -764,7 +819,7 @@ export class Intent { */ public async uploadContent(content: Buffer|string|ReadStream, opts: FileUploadOpts = {}): Promise { await this.ensureRegistered(); - return this.client.uploadContent(content, {...opts, rawResponse: false, onlyContentUri: true}); + return this.botSdkIntent.underlyingClient.uploadContent(content, {...opts, rawResponse: false, onlyContentUri: true}); } /** @@ -774,7 +829,7 @@ export class Intent { */ public async setRoomDirectoryVisibility(roomId: string, visibility: "public"|"private") { await this.ensureRegistered(); - return this.client.setRoomDirectoryVisibility(roomId, visibility); + return this.botSdkIntent.underlyingClient.setDirectoryVisibility(roomId, visibility); } /** @@ -787,6 +842,7 @@ export class Intent { public async setRoomDirectoryVisibilityAppService(roomId: string, networkId: string, visibility: "public"|"private") { await this.ensureRegistered(); + // No function for this yet. return this.client.setRoomDirectoryVisibilityAppService(roomId, visibility, networkId); } @@ -826,7 +882,7 @@ export class Intent { this._membershipStates[event.room_id] = [event.content.membership, profile]; } else if (event.type === "m.room.power_levels") { - this._powerLevels[event.room_id] = event.content as unknown as PowerLevelContent; + this.opts.backingStore.setPowerLevelContent(event.room_id, event.content as unknown as PowerLevelContent); } else if (event.type === "m.room.encryption" && typeof event.content.algorithm === "string") { this.encryptedRooms.set(event.room_id, event.content.algorithm); @@ -854,9 +910,7 @@ export class Intent { roomIdOrAlias: string, ignoreCache = false, viaServers?: string[], passthroughError = false ): Promise { const isRoomId = roomIdOrAlias.startsWith("!"); - const opts: { syncRoom: boolean, viaServers?: string[] } = { - syncRoom: false, - }; + const opts: { viaServers?: string[] } = { }; if (viaServers) { opts.viaServers = viaServers; } @@ -901,7 +955,7 @@ export class Intent { } try { // eslint-disable-next-line camelcase - const { roomId } = await this.client.joinRoom(roomIdOrAlias, opts); + const roomId = await this.botSdkIntent.underlyingClient.joinRoom(roomIdOrAlias, opts.viaServers); mark(roomId, "join"); } catch (ex) { @@ -923,7 +977,7 @@ export class Intent { // eslint-disable-next-line camelcase const { roomId } = await this.botClient.joinRoom(roomIdOrAlias, opts) await this.botClient.invite(roomId, this.userId); - await this.client.joinRoom(roomId, opts); + await this.botSdkIntent.underlyingClient.joinRoom(roomIdOrAlias, opts.viaServers); mark(roomId, "join"); } } @@ -948,7 +1002,7 @@ export class Intent { } const userId = this.userId; const plContent = this.opts.backingStore.getPowerLevelContent(roomId) - || await this.client.getStateEvent(roomId, "m.room.power_levels", ""); + || await this.botSdkIntent.underlyingClient.getRoomStateEvent(roomId, "m.room.power_levels", ""); const eventContent: PowerLevelContent = plContent && typeof plContent === "object" ? plContent : {}; this.opts.backingStore.setPowerLevelContent(roomId, eventContent); const event = { @@ -1024,7 +1078,7 @@ export class Intent { } public async ensureRegistered(forceRegister = false) { - const userId: string = this.client.credentials.userId; + const userId: string = this.userId; log.debug(`Checking if user ${this.client.credentials.userId} is registered`); // We want to skip if and only if all of these conditions are met. // Calling /register twice isn't disasterous, but not calling it *at all* IS. @@ -1034,9 +1088,8 @@ export class Intent { } let registerRes; if (forceRegister || !this.opts.registered) { - const localpart = (new MatrixUser(userId)).localpart; try { - registerRes = await this.botClient.register(localpart); + registerRes = await this.botSdkIntent.ensureRegistered(); this.opts.registered = true; } catch (err) { @@ -1044,10 +1097,10 @@ export class Intent { // Registering the bot will leave it this.opts.registered = true; } - else if (err.errcode === "M_USER_IN_USE") { + else if (err.errcode === "M_USER_IN_USE") { this.opts.registered = true; } - else { + else { throw err; } } From 947c0a5d567d17e10d3be2493c446d769a6faf01 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 23 Apr 2021 18:30:44 +0100 Subject: [PATCH 02/21] More intent.ts fiddling --- package.json | 2 +- src/bridge.ts | 35 ++++--- src/components/intent.ts | 191 ++++++++++++++++++++-------------- yarn.lock | 217 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 346 insertions(+), 99 deletions(-) diff --git a/package.json b/package.json index 96a50b4e..837953cf 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "extend": "^3.0.2", "is-my-json-valid": "^2.20.5", "js-yaml": "^4.0.0", - "matrix-bot-sdk": "^0.5.13", + "matrix-bot-sdk": "^0.5.17", "matrix-appservice": "^0.8.0", "matrix-js-sdk": "^9.5.0", "nedb": "^1.8.0", diff --git a/src/bridge.ts b/src/bridge.ts index 91591974..4433ca82 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -597,7 +597,7 @@ export class Bridge { this.botClient = this.clientFactory.getClientAs(); await this.checkHomeserverSupport(); this.appServiceBot = new AppServiceBot( - this.botClient, this.registration, this.membershipCache, + this.botClient, this.botUserId, this.registration, this.membershipCache, ); if (this.opts.bridgeEncryption) { @@ -1042,7 +1042,7 @@ export class Bridge { * @return The intent instance */ public getIntent(userId?: string, request?: Request) { - if (!this.appServiceBot) { + if (!this.appServiceBot || !this.botSdkAS) { throw Error('Cannot call getIntent before calling .initalise()'); } if (!userId) { @@ -1073,14 +1073,17 @@ export class Bridge { const clientIntentOpts: IntentOpts = { backingStore: this.intentBackingStore, - getJsSdkClient: () => - this.clientFactory.getClientAs( + getJsSdkClient: () => { + if (!this.clientFactory) { + throw Error('clientFactory not ready yet'); + } + return this.clientFactory.getClientAs( userId, request, this.opts.bridgeEncryption?.homeserverUrl, !!this.opts.bridgeEncryption, - ), - + ) + }, ...this.opts.intentOptions?.clients, }; clientIntentOpts.registered = this.membershipCache.isUserRegistered(userId); @@ -1090,11 +1093,15 @@ export class Bridge { sessionPromise: encryptionOpts.store.getStoredSession(userId), sessionCreatedCallback: encryptionOpts.store.setStoredSession.bind(encryptionOpts.store), ensureClientSyncingCallback: async () => { - return this.eeEventBroker?.startSyncingUser(userId!); + return this.eeEventBroker?.startSyncingUser(userId || this.botUserId); }, }; } - const intent = new Intent(client, this.botClient, clientIntentOpts); + const intent = new Intent( + this.botSdkAS.getIntentForUserId(userId), + this.botSdkAS.botClient, + clientIntentOpts, + ); this.intents.set(key, { intent, lastAccessed: Date.now() }); return intent; @@ -1125,10 +1132,11 @@ export class Bridge { matrixUser: MatrixUser, provisionedUser?: {name?: string, url?: string, remote?: RemoteUser} ) { - if (!this.clientFactory) { + if (!this.botSdkAS) { throw Error('Cannot call getIntent before calling .run()'); } - await this.botClient.register(matrixUser.localpart); + const intent = this.botSdkAS.getIntent(matrixUser.localpart); + await intent.ensureRegistered(); if (!this.opts.disableStores) { if (!this.userStore) { @@ -1139,12 +1147,11 @@ export class Bridge { await this.userStore.linkUsers(matrixUser, provisionedUser.remote); } } - const userClient = await this.clientFactory.getClientAs(matrixUser.getId()); if (provisionedUser?.name) { - await userClient.setDisplayName(provisionedUser.name); + await intent.underlyingClient.setDisplayName(provisionedUser.name); } if (provisionedUser?.url) { - await userClient.setAvatarUrl(provisionedUser.url); + await intent.underlyingClient.setAvatarUrl(provisionedUser.url); } } @@ -1185,7 +1192,7 @@ export class Bridge { // we expect some `creationOpts` to create a new room if (!roomId) { // eslint-disable-next-line camelcase - const createRoomResponse: {room_id: string} = await this.botClient.createRoom( + const createRoomResponse: {room_id: string} = await this.botSdkAS?.botClient.createRoom( provisionedRoom.creationOpts ); roomId = createRoomResponse.room_id; diff --git a/src/components/intent.ts b/src/components/intent.ts index 1623722e..23545c45 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -20,21 +20,21 @@ import JsSdk from "matrix-js-sdk"; const { MatrixEvent, RoomMember } = JsSdk as any; import { ClientRequestCache } from "./client-request-cache"; import { defer } from "../utils/promiseutil"; -import { UserMembership, UserProfile } from "./membership-cache"; +import { UserMembership } from "./membership-cache"; import { unstable } from "../errors"; import BridgeErrorReason = unstable.BridgeErrorReason; import { APPSERVICE_LOGIN_TYPE, ClientEncryptionSession } from "./encryption"; import Logging from "./logging"; import { ReadStream } from "fs"; -import BotSdk from "matrix-bot-sdk"; +import BotSdk, { MatrixClient, MatrixProfileInfo, MemoryStorageProvider, PresenceState } from "matrix-bot-sdk"; const log = Logging.get("Intent"); export type IntentBackingStore = { getMembership: (roomId: string, userId: string) => UserMembership, - getMemberProfile: (roomId: string, userid: string) => UserProfile, + getMemberProfile: (roomId: string, userid: string) => MatrixProfileInfo, getPowerLevelContent: (roomId: string) => PowerLevelContent | undefined, - setMembership: (roomId: string, userId: string, membership: UserMembership, profile: UserProfile) => void, + setMembership: (roomId: string, userId: string, membership: UserMembership, profile: MatrixProfileInfo) => void, setPowerLevelContent: (roomId: string, content: PowerLevelContent) => void, }; @@ -64,7 +64,6 @@ export interface RoomCreationOpts { export interface FileUploadOpts { name?: string; - includeFilename?: boolean; type?: string; } @@ -82,7 +81,6 @@ const returnFirstNumber = (...args: unknown[]) => { const DEFAULT_CACHE_TTL = 90000; const DEFAULT_CACHE_SIZE = 1024; -export const APPSERVICE_REGISTER_TYPE = "m.login.application_service"; export type PowerLevelContent = { // eslint-disable-next-line camelcase @@ -103,7 +101,7 @@ type UserProfileKeys = "avatar_url"|"displayname"|null; export class Intent { private _requestCaches: { - profile: ClientRequestCache, + profile: ClientRequestCache, roomstate: ClientRequestCache, event: ClientRequestCache } @@ -121,7 +119,7 @@ export class Intent { registered?: boolean; } // These two are only used if no opts.backingStore is provided to the constructor. - private readonly _membershipStates: Record = {}; + private readonly _membershipStates: Record = {}; private readonly _powerLevels: Record = {}; private readonly encryptedRooms = new Map(); private readonly encryption?: { @@ -135,6 +133,9 @@ export class Intent { // eslint-disable-next-line @typescript-eslint/no-explicit-any private legacyClient?: any; + // This client is used exclusively for E2E requests. + private e2eClient?: MatrixClient; + /** * Create an entity which can fulfil the intent of a given user. * @constructor @@ -182,7 +183,7 @@ export class Intent { */ constructor( public readonly botSdkIntent: BotSdk.Intent, - private readonly botClient: MatrixClient, + private readonly botClient: BotSdk.MatrixClient, opts: IntentOpts = {}) { if (opts.backingStore) { if (!opts.backingStore.setPowerLevelContent || @@ -211,7 +212,8 @@ export class Intent { getPowerLevelContent: (roomId: string) => { return this._powerLevels[roomId]; }, - setMembership: (roomId: string, userId: string, membership: UserMembership, profile: UserProfile) => { + setMembership: ( + roomId: string, userId: string, membership: UserMembership, profile: MatrixProfileInfo) => { if (userId !== this.userId) { return; } @@ -398,7 +400,21 @@ export class Intent { */ public async setPowerLevel(roomId: string, target: string, level: number|undefined) { await this._ensureJoined(roomId); - await this.botSdkIntent.underlyingClient.setUserPowerLevel(roomId, target, level); + const powerLevel: PowerLevelContent = await this.getStateEvent(roomId, "m.room.power_levels", "", true); + if (powerLevel && level && (powerLevel?.users || {})[target] !== level) { + powerLevel.users = powerLevel.users || {}; + powerLevel.users[target] = level; + await this.sendStateEvent(roomId, "m.room.power_levels", "", powerLevel); + } + else if (powerLevel?.users && !level) { + delete powerLevel.users[target]; + await this.sendStateEvent(roomId, "m.room.power_levels", "", powerLevel); + } + else if (!powerLevel && level) { + await this.botSdkIntent.underlyingClient.setUserPowerLevel(target, roomId, level); + } + // Otherwise this is a no-op + log.debug(`Setting PL of ${target} in ${roomId} to ${level} was a no-op`) } /** @@ -504,40 +520,30 @@ export class Intent { */ // eslint-disable-next-line camelcase public async createRoom(opts: RoomCreationOpts): Promise<{room_id: string}> { - const cli = opts.createAsClient ? this.client : this.botClient; - const { userId } = cli.credentials; + const cli = opts.createAsClient ? this.botSdkIntent.underlyingClient : this.botClient; const options = opts.options || {}; if (!opts.createAsClient) { // invite the client if they aren't already options.invite = options.invite || []; - if (Array.isArray(options.invite) && !options.invite.includes(userId)) { - options.invite.push(userId); + if (Array.isArray(options.invite) && !options.invite.includes(this.userId)) { + options.invite.push(this.userId); } } // make sure that the thing doing the room creation isn't inviting itself // else Synapse hard fails the operation with M_FORBIDDEN - if (Array.isArray(options.invite) && options.invite.includes(userId)) { - options.invite.splice(options.invite.indexOf(userId), 1); + if (Array.isArray(options.invite) && options.invite.includes(this.userId)) { + options.invite.splice(options.invite.indexOf(this.userId), 1); } await this.ensureRegistered(); - const res = await cli.createRoom(options); - if (typeof res !== "object" || !res) { - const type = res === null ? "null" : typeof res; - throw Error(`Expected Matrix Server to answer createRoom with an object, got ${type}.`); - } - const roomId = (res as Record).room_id; - if (typeof roomId !== "string") { - const type = typeof roomId; - throw Error(`Expected Matrix Server to answer createRoom with a room_id that is a string, got ${type}.`); - } + const roomId = await cli.createRoom(options); // create a fake power level event to give the room creator ops if we // don't yet have a power level event. if (this.opts.backingStore.getPowerLevelContent(roomId)) { - return res; + return {room_id: roomId}; } const users: Record = {}; - users[userId] = 100; + users[this.userId] = 100; this.opts.backingStore.setPowerLevelContent(roomId, { users_default: 0, events_default: 0, @@ -545,7 +551,7 @@ export class Intent { users: users, events: {} }); - return res; + return {room_id: roomId}; } /** @@ -559,7 +565,7 @@ export class Intent { */ public async invite(roomId: string, target: string) { await this._ensureJoined(roomId); - return this.client.invite(roomId, target); + return this.botSdkIntent.underlyingClient.inviteUser(target, roomId); } /** @@ -577,7 +583,7 @@ export class Intent { // Only ensure joined if we are not also the kicker await this._ensureJoined(roomId); } - return this.client.kick(roomId, target, reason); + return this.botSdkIntent.underlyingClient.kickUser(target, roomId, reason); } /** @@ -592,7 +598,7 @@ export class Intent { */ public async ban(roomId: string, target: string, reason?: string) { await this._ensureJoined(roomId); - return this.client.ban(roomId, target, reason); + return this.botSdkIntent.underlyingClient.banUser(target, roomId, reason); } /** @@ -606,7 +612,7 @@ export class Intent { */ public async unban(roomId: string, target: string) { await this._ensureJoined(roomId); - return this.client.unban(roomId, target); + return this.botSdkIntent.underlyingClient.unbanUser(target, roomId); } /** @@ -631,9 +637,10 @@ export class Intent { */ public async leave(roomId: string, reason?: string) { if (reason) { - return this.kick(roomId, this.userId, reason) + await this.botSdkIntent.ensureRegistered(); + return this.botSdkIntent.underlyingClient.kickUser(this.userId, roomId, reason); } - return this.client.leave(roomId); + return this.botSdkIntent.leaveRoom(roomId); } /** @@ -647,12 +654,20 @@ export class Intent { * @return A Promise that resolves with the requested user's profile * information */ - public async getProfileInfo(userId: string, info: UserProfileKeys = null, useCache = true) { + public async getProfileInfo( + userId: string, info: UserProfileKeys = null, useCache = true): Promise { await this.ensureRegistered(); if (useCache) { - return this._requestCaches.profile.get(`${userId}:${info}`, userId, info); + return this._requestCaches.profile.get(`${userId}`, userId, null); } - return this.client.getProfileInfo(userId, info); + const profile: MatrixProfileInfo = await this.botSdkIntent.underlyingClient.getUserProfile(userId); + if (info === 'avatar_url') { + return { avatar_url: profile.avatar_url }; + } + if (info === 'displayname') { + return { displayname: profile.displayname }; + } + return profile; } /** @@ -662,7 +677,7 @@ export class Intent { */ public async setDisplayName(name: string) { await this.ensureRegistered(); - return this.client.setDisplayName(name); + return this.botSdkIntent.underlyingClient.setDisplayName(name); } /** @@ -672,7 +687,7 @@ export class Intent { */ public async setAvatarUrl(url: string) { await this.ensureRegistered(); - return this.client.setAvatarUrl(url); + return this.botSdkIntent.underlyingClient.setAvatarUrl(url); } /** @@ -694,16 +709,19 @@ export class Intent { } } - public async setRoomUserProfile(roomId: string, profile: UserProfile) { - const userId = this.client.getUserId(); - const currProfile = this.opts.backingStore.getMemberProfile(roomId, userId); + public async setRoomUserProfile(roomId: string, profile: MatrixProfileInfo) { + const currProfile = this.opts.backingStore.getMemberProfile(roomId, this.userId); // Compare the user's current profile (from cache) with the profile // that is requested. Only send the state event if something that was // requested to change is different from the current value. if (("displayname" in profile && currProfile.displayname != profile.displayname) || ("avatar_url" in profile && currProfile.avatar_url != profile.avatar_url)) { - const content = Object.assign({membership: "join"}, currProfile, profile); - await this.client.sendStateEvent(roomId, 'm.room.member', content, userId); + const content = { + membership: "join", + ...currProfile, + ...profile, + }; + await this.sendStateEvent(roomId, 'm.room.member', this.userId, content); } } @@ -714,7 +732,7 @@ export class Intent { */ public async createAlias(alias: string, roomId: string) { await this.ensureRegistered(); - return this.client.createAlias(alias, roomId); + return this.botSdkIntent.underlyingClient.createRoomAlias(alias, roomId); } /** @@ -723,14 +741,13 @@ export class Intent { * @param status_msg The status message to attach. * @return Resolves if the presence was set or no-oped, rejects otherwise. */ - // eslint-disable-next-line camelcase - public async setPresence(presence: string, status_msg?: string) { + public async setPresence(presence: PresenceState, statusMsg?: string) { if (!this.opts.enablePresence) { return undefined; } await this.ensureRegistered(); - return this.client.setPresence({presence, status_msg}); + return this.botSdkIntent.underlyingClient.setPresenceStatus(presence, statusMsg); } /** @@ -781,7 +798,7 @@ export class Intent { if (useCache) { return this._requestCaches.event.get(`${roomId}:${eventId}`, roomId, eventId); } - return this.client.fetchRoomEvent(roomId, eventId); + return this.botSdkIntent.underlyingClient.getEvent(roomId, eventId); } /** @@ -796,7 +813,7 @@ export class Intent { public async getStateEvent(roomId: string, eventType: string, stateKey = "", returnNull = false) { await this._ensureJoined(roomId); try { - return await this.client.getStateEvent(roomId, eventType, stateKey); + return await this.botSdkIntent.underlyingClient.getRoomStateEvent(roomId, eventType, stateKey); } catch (ex) { if (ex.errcode !== "M_NOT_FOUND" || !returnNull) { @@ -843,7 +860,21 @@ export class Intent { */ public async uploadContent(content: Buffer|string|ReadStream, opts: FileUploadOpts = {}): Promise { await this.ensureRegistered(); - return this.botSdkIntent.underlyingClient.uploadContent(content, {...opts, rawResponse: false, onlyContentUri: true}); + let buffer: Buffer; + if (typeof content === "string") { + buffer = Buffer.from(content, "utf8"); + } + else if (content instanceof ReadStream) { + buffer = Buffer.from(content); + } + else { + buffer = content; + } + return this.botSdkIntent.underlyingClient.uploadContent( + buffer, + opts.type, + opts.name, + ); } /** @@ -866,7 +897,7 @@ export class Intent { public async setRoomDirectoryVisibilityAppService(roomId: string, networkId: string, visibility: "public"|"private") { await this.ensureRegistered(); - // No function for this yet. + // XXX: No function for this yet. return this.client.setRoomDirectoryVisibilityAppService(roomId, visibility, networkId); } @@ -899,7 +930,7 @@ export class Intent { if (event.type === "m.room.member" && event.state_key === this.userId && event.content.membership) { - const profile: UserProfile = {}; + const profile: MatrixProfileInfo = {}; if (event.content.displayname) { profile.displayname = event.content.displayname; } @@ -994,16 +1025,14 @@ export class Intent { throw Error("Can't invite via an alias"); } // Try bot inviting client - await this.botClient.invite(roomIdOrAlias, this.userId); - // eslint-disable-next-line camelcase - const { roomId } = await this.client.joinRoom(roomIdOrAlias, opts); + await this.botClient.inviteUser(this.userId, roomIdOrAlias); + const roomId = await this.botClient.joinRoom(roomIdOrAlias, opts.viaServers); mark(roomId, "join"); } catch (_ex) { // Try bot joining - // eslint-disable-next-line camelcase - const { roomId } = await this.botClient.joinRoom(roomIdOrAlias, opts) - await this.botClient.invite(roomId, this.userId); + const roomId = await this.botClient.joinRoom(roomIdOrAlias, opts.viaServers); + await this.botClient.inviteUser(this.userId, roomId); await this.botSdkIntent.underlyingClient.joinRoom(roomIdOrAlias, opts.viaServers); mark(roomId, "join"); } @@ -1060,7 +1089,7 @@ export class Intent { if (requiredLevel > roomMember.powerLevel) { // can the bot update our power level? - const bot = new RoomMember(roomId, this.botClient.credentials.userId); + const bot = new RoomMember(roomId, this.userId); bot.setPowerLevelEvent(powerLevelEvent); const levelRequiredToModifyPowerLevels = returnFirstNumber( // If these are invalid or not provided, default to 0 according to the Spec. @@ -1092,21 +1121,27 @@ export class Intent { private async loginForEncryptedClient() { const userId: string = this.userId; - const res = await this.client.login(APPSERVICE_LOGIN_TYPE, { - identifier: { - type: "m.id.user", - user: userId, - } - }); + const res = await this.botSdkIntent.underlyingClient.doRequest( + "POST", + "/_matrix/client/r0/login", + undefined, + { + type: APPSERVICE_LOGIN_TYPE, + identifier: { + type: "m.id.user", + user: userId, + } + }, + ); return { - accessToken: res.access_token, - deviceId: res.device_id, + accessToken: res.access_token as string, + deviceId: res.device_id as string, }; } public async ensureRegistered(forceRegister = false) { const userId: string = this.userId; - log.debug(`Checking if user ${this.client.credentials.userId} is registered`); + log.debug(`Checking if user ${this.userId} is registered`); // We want to skip if and only if all of these conditions are met. // Calling /register twice isn't disasterous, but not calling it *at all* IS. if (!forceRegister && this.opts.registered && !this.encryption) { @@ -1120,7 +1155,7 @@ export class Intent { this.opts.registered = true; } catch (err) { - if (err.errcode === "M_EXCLUSIVE" && this.botClient === this.client) { + if (err.errcode === "M_EXCLUSIVE" && this.botClient === this.botSdkIntent.underlyingClient) { // Registering the bot will leave it this.opts.registered = true; } @@ -1155,11 +1190,12 @@ export class Intent { let session = await this.encryption.sessionPromise; if (session) { log.debug("ensureRegistered: Existing enc session, reusing"); + const memoryProvider = new MemoryStorageProvider(); // We have existing credentials, set them on the client and run away. - this.client._http.opts.accessToken = session.accessToken; - if (session.syncToken) { - this.client.store.setSyncToken(session.syncToken); - } + memoryProvider.setSyncToken(session.syncToken); + this.e2eClient = new MatrixClient( + this.botClient.homeserverUrl, session.accessToken, memoryProvider + ); } else { this.readyPromise = (async () => { @@ -1178,9 +1214,6 @@ export class Intent { })(); await this.readyPromise; } - // We are using a real user access token. - // We delete the whole extraParams object due to a bug with GET requests - delete this.client._http.opts.extraParams; return undefined; } } diff --git a/yarn.lock b/yarn.lock index da90ce50..cfcedfd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -295,7 +295,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.11", "@types/express@^4.17.8": +"@types/express@^4.17.11", "@types/express@^4.17.7", "@types/express@^4.17.8": version "4.17.11" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.11.tgz#debe3caa6f8e5fcda96b47bd54e2f40c4ee59545" integrity sha512-no+R6rW60JEc59977wIxreQVsIEOAYwgCqldrA/vkpCnbD7MqTefO97lmoBe4WE0F156bC4uLSP1XHDOySnChg== @@ -634,6 +634,11 @@ bintrees@1.0.1: resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" @@ -731,7 +736,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -911,6 +916,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + default-require-extensions@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.0.tgz#e03f93aac9b2b6443fc52e5e4a37b3ad9ad8df96" @@ -952,6 +962,43 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-serializer@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.1.tgz#d845a1565d7c041a95e5dab62184ab41e3a519be" + integrity sha512-Pv2ZluG5ife96udGgEDovOOOA5UELkltfJpnIExPrAk1LTvecolUGn6lIaoLh86d83GiB86CjzciMd9BuRB71Q== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domhandler@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" + integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== + dependencies: + domelementtype "^2.0.1" + +domhandler@^4.0.0, domhandler@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059" + integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.0.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" + integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -992,6 +1039,11 @@ enquirer@^2.3.5: dependencies: ansi-colors "^4.1.1" +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + es6-error@^4.0.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" @@ -1411,6 +1463,11 @@ glob-parent@^5.0.0, glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -1454,7 +1511,7 @@ globby@^11.0.1: merge2 "^1.3.0" slash "^3.0.0" -graceful-fs@^4.1.15, graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.15, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -1506,6 +1563,14 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -1514,11 +1579,42 @@ hasha@^5.0.0: is-stream "^2.0.0" type-fest "^0.8.0" +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== +html-to-text@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-6.0.0.tgz#8b48adb1b781a8378f374c5bb481864a169f59f4" + integrity sha512-r0KNC5aqCAItsjlgtirW6RW25c92Ee3ybQj8z//4Sl4suE3HIPqM4deGpYCUJULLjtVPEP1+Ma+1ZeX1iMsCiA== + dependencies: + deepmerge "^4.2.2" + he "^1.2.0" + htmlparser2 "^4.1.0" + lodash "^4.17.20" + minimist "^1.2.5" + +htmlencode@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/htmlencode/-/htmlencode-0.0.4.tgz#f7e2d6afbe18a87a78e63ba3308e753766740e3f" + integrity sha1-9+LWr74YqHp45jujMI51N2Z0Dj8= + +htmlparser2@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" + integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== + dependencies: + domelementtype "^2.0.1" + domhandler "^3.0.0" + domutils "^2.0.0" + entities "^2.0.0" + http-errors@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" @@ -1668,6 +1764,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-promise@^2.1.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" + integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ== + is-property@^1.0.0, is-property@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" @@ -1902,7 +2003,7 @@ lodash.flattendeep@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: +lodash@4, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1923,6 +2024,17 @@ loglevel@^1.7.1: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw== +lowdb@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" + integrity sha512-2+x8esE/Wb9SQ1F9IHaYWfsC9FIecLOPrK4g17FGEayjUWH172H6nwicRovGvSE2CPZouc2MCIqCI7h9d+GftQ== + dependencies: + graceful-fs "^4.1.3" + is-promise "^2.1.0" + lodash "4" + pify "^3.0.0" + steno "^0.4.1" + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -1965,6 +2077,26 @@ matrix-appservice@^0.8.0: js-yaml "^3.14.0" morgan "^1.10.0" +matrix-bot-sdk@^0.5.13: + version "0.5.17" + resolved "https://registry.yarnpkg.com/matrix-bot-sdk/-/matrix-bot-sdk-0.5.17.tgz#ce5d1eb9e8fef5c46f0e553493798bf0cd98ea00" + integrity sha512-6Ze0D9OmE/ssOVSn7yIbApjZIKaCrRT2H0GVxOVVSUbRnNHSR0pgfCofO18SSI9zWhzpugiKyocZxNesrppa4A== + dependencies: + "@types/express" "^4.17.7" + chalk "^4.1.0" + express "^4.17.1" + glob-to-regexp "^0.4.1" + hash.js "^1.1.7" + html-to-text "^6.0.0" + htmlencode "^0.0.4" + lowdb "^1.0.0" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + morgan "^1.10.0" + request "^2.88.2" + request-promise "^4.2.6" + sanitize-html "^1.27.2" + matrix-js-sdk@^9.5.0: version "9.9.0" resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.9.0.tgz#30c46419c026fcad0ff25aea9417b77921f64c6d" @@ -2025,6 +2157,11 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + minimatch@^3.0.0, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -2037,6 +2174,11 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -2281,6 +2423,11 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -2326,6 +2473,11 @@ picomatch@^2.0.5, picomatch@^2.2.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -2333,6 +2485,15 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" +postcss@^7.0.27: + version "7.0.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" + integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2463,6 +2624,23 @@ release-zalgo@^1.0.0: dependencies: es6-error "^4.0.1" +request-promise-core@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" + integrity sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw== + dependencies: + lodash "^4.17.19" + +request-promise@^4.2.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.6.tgz#7e7e5b9578630e6f598e3813c0f8eb342a27f0a2" + integrity sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ== + dependencies: + bluebird "^3.5.0" + request-promise-core "1.1.4" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -2556,6 +2734,16 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-html@^1.27.2: + version "1.27.5" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.27.5.tgz#6c8149462adb23e360e1bb71cc0bae7f08c823c7" + integrity sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A== + dependencies: + htmlparser2 "^4.1.0" + lodash "^4.17.15" + parse-srcset "^1.0.2" + postcss "^7.0.27" + semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -2723,6 +2911,18 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +steno@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" + integrity sha1-BxEFvfwobmYVwEA8J+nXtdy4Vcs= + dependencies: + graceful-fs "^4.1.3" + string-width@^4.1.0, string-width@^4.2.0: version "4.2.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" @@ -2770,6 +2970,13 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -2830,7 +3037,7 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@~2.5.0: +tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== From a3dfda761b4241ca25b1a8d420b694145a38ec30 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 3 May 2021 15:51:39 +0100 Subject: [PATCH 03/21] bits and bobs --- src/bridge.ts | 1 - src/components/app-service-bot.ts | 10 +++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index 4433ca82..ef4ee8d7 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -399,7 +399,6 @@ export class Bridge { private botIntent?: Intent; private appServiceBot?: AppServiceBot; private clientFactory?: ClientFactory; - private botClient?: any; private metrics?: PrometheusMetrics; private roomLinkValidator?: RoomLinkValidator; private roomUpgradeHandler?: RoomUpgradeHandler; diff --git a/src/components/app-service-bot.ts b/src/components/app-service-bot.ts index 0e34a1ab..7302b0d1 100644 --- a/src/components/app-service-bot.ts +++ b/src/components/app-service-bot.ts @@ -58,7 +58,7 @@ export class AppServiceBot { * @return Resolves to a list of room IDs. */ public async getJoinedRooms(): Promise { - return (await this.client.getJoinedRooms()).joined_rooms || []; + return await this.client.getJoinedRooms(); } /** @@ -67,13 +67,9 @@ export class AppServiceBot { * @param roomId The room to get a list of joined user IDs in. * @return Resolves to a map of user ID => display_name avatar_url */ - public async getJoinedMembers(roomId: string) { + public async getJoinedMembers(roomId: string, includeProfile = true) { // eslint-disable-next-line camelcase - const res: {joined: Record} - = await this.client.getJoinedRoomMembers(roomId); - if (!res.joined) { - return {}; - } + const res = await this.client.getJoinedRoomMembers(roomId); for (const [member, p] of Object.entries(res.joined)) { if (this.isRemoteUser(member)) { const profile: UserProfile = {}; From d10d15c8193b7588a1fffca1026728e97f2eded9 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 11 May 2021 17:30:43 +0100 Subject: [PATCH 04/21] Make everything work with new intents --- src/bridge.ts | 42 ++++++++++++++++++------------- src/components/app-service-bot.ts | 4 +-- src/components/intent.ts | 22 ++++++++-------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index ef4ee8d7..c50208c7 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -51,7 +51,7 @@ import { RemoteRoom } from "./models/rooms/remote"; import { Registry } from "prom-client"; import { ClientEncryptionStore, EncryptedEventBroker } from "./components/encryption"; import { EphemeralEvent, PresenceEvent, ReadReceiptEvent, TypingEvent, WeakEvent } from "./components/event-types"; -import BotSDK from "matrix-bot-sdk"; +import * as BotSDK from "matrix-bot-sdk"; const log = logging.get("bridge"); @@ -564,6 +564,7 @@ export class Bridge { throw Error('No AS token provided, cannot create ClientFactory'); } const rawReg = this.registration.getOutput(); + console.log("WOOOOFSH:", JSON.stringify(rawReg)); this.botSdkAS = new BotSDK.Appservice({ registration: { ...rawReg, @@ -593,10 +594,9 @@ export class Bridge { this.clientFactory.setLogFunction((text, isErr) => { this.onLog(text, isErr || false); }); - this.botClient = this.clientFactory.getClientAs(); await this.checkHomeserverSupport(); this.appServiceBot = new AppServiceBot( - this.botClient, this.botUserId, this.registration, this.membershipCache, + this.botSdkAS.botClient, this.botUserId, this.registration, this.membershipCache, ); if (this.opts.bridgeEncryption) { @@ -634,13 +634,17 @@ export class Bridge { ) ); } - const botIntentOpts: IntentOpts = { - registered: true, - backingStore: this.intentBackingStore, - ...this.opts.intentOptions?.bot, // copy across opts, if defined - }; - this.botIntent = new Intent(this.botClient, this.botClient, botIntentOpts); + this.botIntent = new Intent( + this.botSdkAS.botIntent, + this.botSdkAS.botClient, + { + registered: true, + backingStore: this.intentBackingStore, + getJsSdkClient: () => this.clientFactory?.getClientAs(), + ...this.opts.intentOptions?.bot, // copy across opts, if defined + } + ); this.setupIntentCulling(); @@ -1189,12 +1193,15 @@ export class Bridge { let roomId = provisionedRoom.roomId; // If they didn't pass an existing `roomId` back, // we expect some `creationOpts` to create a new room - if (!roomId) { - // eslint-disable-next-line camelcase - const createRoomResponse: {room_id: string} = await this.botSdkAS?.botClient.createRoom( + if (roomId === undefined) { + roomId = await this.botSdkAS?.botClient.createRoom( provisionedRoom.creationOpts ); - roomId = createRoomResponse.room_id; + } + + if (!roomId) { + // In theory this should never be called, but typescript isn't happy. + throw Error('Expected roomId to be defined'); } if (!this.opts.disableStores) { @@ -1507,13 +1514,14 @@ export class Bridge { public async checkHomeserverSupport() { - if (!this.botClient) { - throw Error("botClient isn't ready yet"); + if (!this.botSdkAS) { + throw Error("botSdkAS isn't ready yet"); } // Min required version if (this.opts.bridgeEncryption) { // Ensure that we have support for /login - const loginFlows: {flows: {type: string}[]} = await this.botClient.loginFlows(); + const loginFlows: {flows: {type: string}[]} = + await this.botSdkAS.botClient.doRequest("GET", "/_matrix/client/r0/login"); if (!EncryptedEventBroker.supportsLoginFlow(loginFlows)) { throw Error('To enable support for encryption, your homeserver must support MSC2666'); } @@ -1530,7 +1538,7 @@ export class Bridge { */ public async pingAppserviceRoute(roomId: string, timeoutMs = BRIDGE_PING_TIMEOUT_MS) { if (!this.botIntent) { - throw Error("botClient isn't ready yet"); + throw Error("botIntent isn't ready yet"); } const sentTs = Date.now(); if (this.selfPingDeferred) { diff --git a/src/components/app-service-bot.ts b/src/components/app-service-bot.ts index 7302b0d1..0c3552b7 100644 --- a/src/components/app-service-bot.ts +++ b/src/components/app-service-bot.ts @@ -67,9 +67,9 @@ export class AppServiceBot { * @param roomId The room to get a list of joined user IDs in. * @return Resolves to a map of user ID => display_name avatar_url */ - public async getJoinedMembers(roomId: string, includeProfile = true) { + public async getJoinedMembers(roomId: string) { // eslint-disable-next-line camelcase - const res = await this.client.getJoinedRoomMembers(roomId); + const res = await this.client.getJoinedRoomMembersWithProfiles(roomId); for (const [member, p] of Object.entries(res.joined)) { if (this.isRemoteUser(member)) { const profile: UserProfile = {}; diff --git a/src/components/intent.ts b/src/components/intent.ts index 23545c45..a9ee85e3 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -26,7 +26,7 @@ import BridgeErrorReason = unstable.BridgeErrorReason; import { APPSERVICE_LOGIN_TYPE, ClientEncryptionSession } from "./encryption"; import Logging from "./logging"; import { ReadStream } from "fs"; -import BotSdk, { MatrixClient, MatrixProfileInfo, MemoryStorageProvider, PresenceState } from "matrix-bot-sdk"; +import BotSdk, { MatrixClient, MatrixProfileInfo, PresenceState } from "matrix-bot-sdk"; const log = Logging.get("Intent"); @@ -133,13 +133,10 @@ export class Intent { // eslint-disable-next-line @typescript-eslint/no-explicit-any private legacyClient?: any; - // This client is used exclusively for E2E requests. - private e2eClient?: MatrixClient; - /** * Create an entity which can fulfil the intent of a given user. * @constructor - * @param client The bot sdk intent which this intent wraps + * @param botSdkIntent The bot sdk intent which this intent wraps * fulfilled e.g. the entity joining the room when you call intent.join(roomId). * @param botClient The client instance for the AS bot itself. * This will be used to perform more priveleged actions such as creating new @@ -180,6 +177,7 @@ export class Intent { * * @param opts.caching.ttl How long requests can stay in the cache, in milliseconds. * @param opts.caching.size How many entries should be kept in the cache, before the oldest is dropped. + * @param opts.getJsSdkClient Create a Matrix JS SDK client on demand for legacy code. */ constructor( public readonly botSdkIntent: BotSdk.Intent, @@ -1107,9 +1105,10 @@ export class Intent { "edit the client's power level." ); } + // TODO: This might be inefficent. // update the client's power level first - await this.botClient.setPowerLevel( - roomId, userId, requiredLevel, powerLevelEvent + await this.botSdkIntent.underlyingClient.setUserPowerLevel( + userId, roomId, requiredLevel, ); // tweak the level for the client to reflect the new reality const userLevels = powerLevelEvent.getContent().users || {}; @@ -1190,12 +1189,11 @@ export class Intent { let session = await this.encryption.sessionPromise; if (session) { log.debug("ensureRegistered: Existing enc session, reusing"); - const memoryProvider = new MemoryStorageProvider(); // We have existing credentials, set them on the client and run away. - memoryProvider.setSyncToken(session.syncToken); - this.e2eClient = new MatrixClient( - this.botClient.homeserverUrl, session.accessToken, memoryProvider - ); + // We need to overwrite the access token here. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.botSdkIntent.underlyingClient as any).accessToken = session.accessToken; + this.botSdkIntent.underlyingClient.storageProvider.setSyncToken(session.syncToken); } else { this.readyPromise = (async () => { From eecc972b005558aaa1ee65a70ba30aa5dc77c81d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 25 May 2021 17:18:16 +0100 Subject: [PATCH 05/21] More test bits --- spec/integ/bridge.spec.js | 90 ++++---- spec/unit/intent.spec.js | 469 +++++++++++++++++++------------------- src/bridge.ts | 7 +- src/components/intent.ts | 175 +++++++------- 4 files changed, 369 insertions(+), 372 deletions(-) diff --git a/spec/integ/bridge.spec.js b/spec/integ/bridge.spec.js index 214f648b..f9cdd253 100644 --- a/spec/integ/bridge.spec.js +++ b/spec/integ/bridge.spec.js @@ -1,7 +1,6 @@ "use strict"; const Datastore = require("nedb"); const fs = require("fs"); -const log = require("../log"); const HS_URL = "http://example.com"; const HS_DOMAIN = "example.com"; @@ -22,26 +21,21 @@ const {Bridge, BRIDGE_PING_EVENT_TYPE, BRIDGE_PING_TIMEOUT_MS} = require("../.." const deferPromise = require("../../lib/utils/promiseutil").defer; describe("Bridge", function() { - var bridge, bridgeCtrl, appService, clientFactory, appServiceRegistration; - var roomStore, userStore, clients; + let bridge, bridgeCtrl, appService, clientFactory, appServiceRegistration, intents; + let roomStore, userStore, clients; - beforeEach( - /** @this */ - function(done) { - log.beforeEach(this); + beforeEach(async function() { // Setup mock client factory to avoid making real outbound HTTP conns - clients = {}; clientFactory = jasmine.createSpyObj("ClientFactory", [ "setLogFunction", "getClientAs", "configure" ]); - clientFactory.getClientAs.and.callFake(function(uid, req) { - return clients[ - (uid ? uid : "bot") + (req ? req.getId() : "")] || {uid}; + botSdk = jasmine.createSpyObj("ClientFactory", [ + "setLogFunction", "getClientAs", "configure" + ]); + clientFactory.getClientAs.and.callFake(function() { + // We shouldn't need this for any of these + throw Error('Not able to fetch legacy client'); }); - clients["bot"] = mkMockMatrixClient( - "@" + BOT_LOCALPART + ":" + HS_DOMAIN - ); - // Setup mock AppService to avoid listening on a real port appService = jasmine.createSpyObj("AppService", [ "onAliasQuery", "onUserQuery", "listen", "on" @@ -81,7 +75,7 @@ describe("Bridge", function() { function loadDatabase(path, Cls) { const defer = deferPromise(); - var db = new Datastore({ + const db = new Datastore({ filename: path, autoload: true, onload: function(err) { @@ -95,25 +89,22 @@ describe("Bridge", function() { return defer.promise; } - Promise.all([ - loadDatabase(TEST_USER_DB_PATH, UserBridgeStore), - loadDatabase(TEST_ROOM_DB_PATH, RoomBridgeStore) - ]).then(([userDb, roomDb]) => { - userStore = userDb; - roomStore = roomDb; - bridge = new Bridge({ - homeserverUrl: HS_URL, - domain: HS_DOMAIN, - registration: appServiceRegistration, - userStore: userDb, - roomStore: roomDb, - controller: bridgeCtrl, - clientFactory: clientFactory - }); - return bridge.loadDatabases(); - }).then(() => { - done(); - }); + userStore = await loadDatabase(TEST_USER_DB_PATH, UserBridgeStore); + roomStore = await loadDatabase(TEST_ROOM_DB_PATH, RoomBridgeStore); + bridge = new Bridge({ + homeserverUrl: HS_URL, + domain: HS_DOMAIN, + registration: appServiceRegistration, + userStore, + roomStore, + controller: bridgeCtrl, + clientFactory: clientFactory + }); + await bridge.loadDatabases(); + await bridge.initalise(); + // Mock the BotSdk Intents + // --- + intents = bridge.intents; }); afterEach(function() { @@ -133,7 +124,7 @@ describe("Bridge", function() { describe("onUserQuery", function() { it("should invoke the user-supplied onUserQuery function with the right args", async() => { - await bridge.run(101, {}, appService); + await bridge.listen(101, "127.0.0.1", 10, appService); try { await appService.onUserQuery("@alice:bar"); } @@ -142,28 +133,30 @@ describe("Bridge", function() { } finally { expect(bridgeCtrl.onUserQuery).toHaveBeenCalled(); - var call = bridgeCtrl.onUserQuery.calls.argsFor(0); - var mxUser = call[0]; + const [mxUser] = bridgeCtrl.onUserQuery.calls.argsFor(0); expect(mxUser.getId()).toEqual("@alice:bar"); } }); it("should not provision a user if null is returned from the function", - async function(done) { + async function() { + await bridge.listen(101, "127.0.0.1", 10, appService); bridgeCtrl.onUserQuery.and.returnValue(null); await bridge.run(101, {}, appService); - appService.onUserQuery("@alice:bar").catch(function() {}).finally(function() { - expect(clients["bot"].register).not.toHaveBeenCalled(); - done(); - }); + try { + await appService.onUserQuery("@alice:bar"); + } + catch (ex) { + //... + } + expect(intents.keys()).not.toContain("@alice:bar"); }); it("should provision the user from the return object", async() => { bridgeCtrl.onUserQuery.and.returnValue({}); - clients["bot"].register.and.returnValue(Promise.resolve({})); - await bridge.run(101, {}, appService); + await bridge.listen(101, "127.0.0.1", 10, appService); await appService.onUserQuery("@alice:bar"); - expect(clients["bot"].register).toHaveBeenCalledWith("alice"); + expect(intents.keys()).toContain("@alice:bar"); }); }); @@ -789,7 +782,7 @@ describe("Bridge", function() { }); function mkMockMatrixClient(uid) { - var client = jasmine.createSpyObj( + const client = jasmine.createSpyObj( "MatrixClient", [ "register", "joinRoom", "credentials", "createRoom", "setDisplayName", "setAvatarUrl", "_http" @@ -797,6 +790,9 @@ function mkMockMatrixClient(uid) { ); // Shim requests to authedRequestWithPrefix to register() if it is // directed at /register + // client._http.authedRequest.and.callFake(((method, endpoint, qs, body, timeout, raw, contentType, noEncoding) => { + // console.log(method, endpoint, qs, body, timeout, raw, contentType, noEncoding); + // }); client._http.authedRequest = jasmine.createSpy("authedRequest"); client._http.authedRequest.and.callFake(function(a, method, path, d, data) { if (method === "POST" && path === "/register") { diff --git a/spec/unit/intent.spec.js b/spec/unit/intent.spec.js index 78a4d9fa..b010d3a4 100644 --- a/spec/unit/intent.spec.js +++ b/spec/unit/intent.spec.js @@ -1,8 +1,7 @@ const { Intent } = require("../.."); -const log = require("../log"); describe("Intent", function() { - let intent, client, botClient; + let intent, botIntent, client, botClient; const userId = "@alice:bar"; const botUserId = "@bot:user"; const roomId = "!foo:bar"; @@ -13,27 +12,44 @@ describe("Intent", function() { beforeEach( /** @this */ function() { - log.beforeEach(this); const clientFields = [ - "credentials", "joinRoom", "invite", "leave", "ban", "unban", - "kick", "getStateEvent", "setPowerLevel", "sendTyping", "sendEvent", - "sendStateEvent", "setDisplayName", "setAvatarUrl", + "credentials", "joinRoom", "resolveRoom", "inviteUser", "leave", "ban", "unban", + "kick", "getRoomStateEvent", "setUserPowerLevel", "sendTyping", "sendEvent", + "sendStateEvent", "setDisplayName", "setAvatarUrl", "getUserId", ]; - client = jasmine.createSpyObj("client", clientFields); + client = jasmine.createSpyObj("botSdkIntentClient", clientFields); + botIntent = { + userId, + underlyingClient: client, + }; client.credentials.userId = userId; botClient = jasmine.createSpyObj("botClient", clientFields); - botClient.credentials.userId = botUserId; - intent = new Intent(client, botClient, alreadyRegistered); + botClient.resolveRoom.and.callFake(async () => roomId); + botClient.getUserId.and.callFake(async () => botUserId); + intent = new Intent(botIntent, botClient, alreadyRegistered); }); describe("join", function() { it("should /join/$ROOMID if it doesn't know it is already joined", function() { - client.joinRoom.and.callFake(() => Promise.resolve({roomId: roomId})); + client.joinRoom.and.callFake(async () => roomId); return intent.join(roomId).then(function(resultRoomId) { expect(client.joinRoom).toHaveBeenCalledWith( - roomId, { syncRoom: false } + roomId, undefined, + ); + expect(resultRoomId).toBe(roomId); + }); + }); + it("should /join/$ROOMID if it doesn't know it is already joined with via parameters", + function() { + const via = ["foo.org", "bar.org"]; + client.joinRoom.and.callFake(async () =>{ + return roomId; + }); + return intent.join(roomId, via).then(function(resultRoomId) { + expect(client.joinRoom).toHaveBeenCalledWith( + roomId, via, ); expect(resultRoomId).toBe(roomId); }); @@ -70,21 +86,21 @@ describe("Intent", function() { it("should make the bot invite then the client join", function() { client.joinRoom.and.callFake(function() { - if (botClient.invite.calls.count() === 0) { + if (botClient.inviteUser.calls.count() === 0) { return Promise.reject({ errcode: "M_FORBIDDEN", error: "Join first" }); } - return Promise.resolve({roomId: roomId}); + return Promise.resolve(roomId); }); - botClient.invite.and.callFake(() => Promise.resolve({})); + botClient.inviteUser.and.callFake(() => Promise.resolve({})); return intent.join(roomId).then(function(resultRoomId) { expect(client.joinRoom).toHaveBeenCalledWith( - roomId, { syncRoom: false } + roomId, undefined ); - expect(botClient.invite).toHaveBeenCalledWith(roomId, userId); + expect(botClient.inviteUser).toHaveBeenCalledWith(userId, roomId); expect(resultRoomId).toBe(roomId); }); }); @@ -93,7 +109,7 @@ describe("Intent", function() { it("should make the bot join then invite then the client join", function() { client.joinRoom.and.callFake(function() { - if (botClient.invite.calls.count() === 0) { + if (botClient.inviteUser.calls.count() === 0) { return Promise.reject({ errcode: "M_FORBIDDEN", error: "Join first" @@ -101,7 +117,7 @@ describe("Intent", function() { } return Promise.resolve({}); }); - botClient.invite.and.callFake(function() { + botClient.inviteUser.and.callFake(function() { if (botClient.joinRoom.calls.count() === 0) { return Promise.reject({ errcode: "M_FORBIDDEN", @@ -114,11 +130,11 @@ describe("Intent", function() { return intent.join(roomId).then(function(resultRoomId) { expect(client.joinRoom).toHaveBeenCalledWith( - roomId, { syncRoom: false } + roomId, undefined ); - expect(botClient.invite).toHaveBeenCalledWith(roomId, userId); + expect(botClient.inviteUser).toHaveBeenCalledWith(userId, roomId); expect(botClient.joinRoom).toHaveBeenCalledWith( - roomId, { syncRoom: false } + roomId, undefined ); expect(resultRoomId).toBe(roomId); @@ -130,7 +146,7 @@ describe("Intent", function() { errcode: "M_FORBIDDEN", error: "Join first" })); - botClient.invite.and.callFake(() => Promise.reject({ + botClient.inviteUser.and.callFake(() => Promise.reject({ errcode: "M_FORBIDDEN", error: "No invites kthx" })); @@ -141,11 +157,11 @@ describe("Intent", function() { return intent.join(roomId).catch(function() { expect(client.joinRoom).toHaveBeenCalledWith( - roomId, { syncRoom: false } + roomId, undefined ); - expect(botClient.invite).toHaveBeenCalledWith(roomId, userId); + expect(botClient.inviteUser).toHaveBeenCalledWith(userId, roomId); expect(botClient.joinRoom).toHaveBeenCalledWith( - roomId, { syncRoom: false } + roomId, undefined ); }); }); @@ -197,241 +213,222 @@ describe("Intent", function() { it("should directly send the event if it thinks power levels are ok", function() { - client.sendStateEvent.and.returnValue(Promise.resolve({})); + client.sendStateEvent.and.returnValue(Promise.resolve("$foo:bar")); intent.onEvent(validPowerLevels); return intent.setRoomTopic(roomId, "Hello world").then(function() { expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, "m.room.topic", {topic: "Hello world"}, "" - ); - }) - }); - - it("should get the power levels before sending if it doesn't know them", - function() { - client.sendStateEvent.and.returnValue(Promise.resolve({})); - client.getStateEvent.and.returnValue( - Promise.resolve(validPowerLevels.content) - ); - - return intent.setRoomTopic(roomId, "Hello world").then(function() { - expect(client.getStateEvent).toHaveBeenCalledWith( - roomId, "m.room.power_levels", "" - ); - expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, "m.room.topic", {topic: "Hello world"}, "" + roomId, "m.room.topic", "", {topic: "Hello world"} ); }) }); it("should modify power levels before sending if client is too low", - function() { + async function() { client.sendStateEvent.and.callFake(function() { - if (botClient.setPowerLevel.calls.count() > 0) { + if (client.sendStateEvent.calls.count() > 1) { return Promise.resolve({}); } + console.log("ARGH", client.sendStateEvent.calls.count()); return Promise.reject({ errcode: "M_FORBIDDEN", error: "Not enough powaaaaaa" }); }); - botClient.setPowerLevel.and.returnValue(Promise.resolve({})); + botClient.setUserPowerLevel.and.returnValue(Promise.resolve({})); // give the power to the bot invalidPowerLevels.content.users[botUserId] = 100; intent.onEvent(invalidPowerLevels); - return intent.setRoomTopic(roomId, "Hello world").then(function() { - expect(client.sendStateEvent).toHaveBeenCalledWith( - roomId, "m.room.topic", {topic: "Hello world"}, "" - ); - expect(botClient.setPowerLevel).toHaveBeenCalledWith( - roomId, userId, 50, jasmine.any(Object) - ); - }) - }); - - it("should fail if the bot cannot modify power levels and the client is too low", - function() { - // bot has NO power - intent.onEvent(invalidPowerLevels); - - return intent.setRoomTopic(roomId, "Hello world").catch(function() { - expect(client.sendStateEvent).not.toHaveBeenCalled(); - expect(botClient.setPowerLevel).not.toHaveBeenCalled(); - }) - }); - }); - - describe("sending message events", function() { - const content = { - body: "hello world", - msgtype: "m.text", - }; - - beforeEach(function() { - intent = new Intent(client, botClient, { - ...alreadyRegistered, - dontCheckPowerLevel: true, - }); - // not interested in joins, so no-op them. - intent.onEvent({ - event_id: "test", - type: "m.room.member", - state_key: userId, - room_id: roomId, - content: { - membership: "join" - } - }); - }); - - it("should immediately try to send the event if joined/have pl", function() { - client.sendEvent.and.returnValue(Promise.resolve({ - event_id: "$abra:kadabra" - })); - return intent.sendMessage(roomId, content).then(function() { - expect(client.sendEvent).toHaveBeenCalledWith( - roomId, "m.room.message", content - ); - expect(client.joinRoom).not.toHaveBeenCalled(); - }); - }); - - it("should fail if get an error that isn't M_FORBIDDEN", function() { - client.sendEvent.and.callFake(() => Promise.reject({ - error: "Oh no", - errcode: "M_UNKNOWN" - })); - return intent.sendMessage(roomId, content).catch(function() { - expect(client.sendEvent).toHaveBeenCalledWith( - roomId, "m.room.message", content - ); - expect(client.joinRoom).not.toHaveBeenCalled(); - }); - }); - - it("should try to join the room on M_FORBIDDEN then resend", function() { - let isJoined = false; - client.sendEvent.and.callFake(function() { - if (isJoined) { - return Promise.resolve({ - event_id: "$12345:6789" - }); - } - return Promise.reject({ - error: "You are not joined", - errcode: "M_FORBIDDEN" - }); - }); - client.joinRoom.and.callFake(function(joinRoomId) { - isJoined = true; - return Promise.resolve({ - roomId: joinRoomId, - }); - }); - return intent.sendMessage(roomId, content).then(function(eventId) { - expect(client.sendEvent).toHaveBeenCalledWith( - roomId, "m.room.message", content - ); - expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false }); - expect(eventId).toEqual({event_id: "$12345:6789"}); - }); + await intent.setRoomTopic(roomId, "Hello world"); + console.log("ABC"); + expect(client.sendStateEvent).toHaveBeenCalledWith( + roomId, "m.room.topic", "", {topic: "Hello world"} + ); + console.log("ABC"); + expect(botClient.setUserPowerLevel).toHaveBeenCalledWith( + userId, roomId, 50 + ); }); - it("should fail if the join on M_FORBIDDEN fails", function() { - client.sendEvent.and.callFake(function() { - return Promise.reject({ - error: "You are not joined", - errcode: "M_FORBIDDEN" - }); - }); - client.joinRoom.and.callFake(() => Promise.reject({ - error: "Never!", - errcode: "M_YOU_ARE_A_FISH" - })); - return intent.sendMessage(roomId, content).catch(function() { - expect(client.sendEvent).toHaveBeenCalledWith( - roomId, "m.room.message", content - ); - expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false }); - }); - }); + // it("should fail if the bot cannot modify power levels and the client is too low", + // function() { + // // bot has NO power + // intent.onEvent(invalidPowerLevels); - it("should fail if the resend after M_FORBIDDEN fails", function() { - let isJoined = false; - client.sendEvent.and.callFake(function() { - if (isJoined) { - return Promise.reject({ - error: "Internal Server Error", - errcode: "M_WHOOPSIE", - }); - } - return Promise.reject({ - error: "You are not joined", - errcode: "M_FORBIDDEN", - }); - }); - client.joinRoom.and.callFake(function(joinRoomId) { - isJoined = true; - return Promise.resolve({ - roomId: joinRoomId, - }); - }); - return intent.sendMessage(roomId, content).catch(function() { - expect(client.sendEvent).toHaveBeenCalledWith( - roomId, "m.room.message", content - ); - expect(client.joinRoom).toHaveBeenCalledWith(roomId, { syncRoom: false }); - }); - }); + // return intent.setRoomTopic(roomId, "Hello world").catch(function() { + // expect(client.sendStateEvent).not.toHaveBeenCalled(); + // expect(botClient.setUserPowerLevel).not.toHaveBeenCalled(); + // }) + // }); }); - describe("signaling bridge error", function() { - const reason = "m.event_not_handled" - let affectedUsers, eventId, bridge; - - beforeEach(function() { - intent = new Intent(client, botClient, { - ...alreadyRegistered, - dontCheckPowerLevel: true, - }); - // not interested in joins, so no-op them. - intent.onEvent({ - event_id: "test", - type: "m.room.member", - state_key: userId, - room_id: roomId, - content: { - membership: "join" - } - }); - eventId = "$random:event.id"; - bridge = "International Pidgeon Post"; - affectedUsers = ["@_pidgeonpost_.*:home.server"]; - }); - - it("should send an event", function() { - client.sendEvent.and.returnValue(Promise.resolve({ - event_id: "$abra:kadabra" - })); - return intent - .unstableSignalBridgeError(roomId, eventId, bridge, reason, affectedUsers) - .then(() => { - expect(client.sendEvent).toHaveBeenCalledWith( - roomId, - "de.nasnotfound.bridge_error", - { - "network_name": bridge, - "reason": reason, - "affected_users": affectedUsers, - "m.relates_to": { - "rel_type": "m.reference", - "event_id": eventId - } - } - ); - expect(client.joinRoom).not.toHaveBeenCalled(); - }); - }); - }); + // describe("sending message events", function() { + // const content = { + // body: "hello world", + // msgtype: "m.text", + // }; + + // beforeEach(function() { + // intent = new Intent(client, botClient, { + // ...alreadyRegistered, + // dontCheckPowerLevel: true, + // }); + // // not interested in joins, so no-op them. + // intent.onEvent({ + // event_id: "test", + // type: "m.room.member", + // state_key: userId, + // room_id: roomId, + // content: { + // membership: "join" + // } + // }); + // }); + + // it("should immediately try to send the event if joined/have pl", function() { + // client.sendEvent.and.returnValue(Promise.resolve({ + // event_id: "$abra:kadabra" + // })); + // return intent.sendMessage(roomId, content).then(function() { + // expect(client.sendEvent).toHaveBeenCalledWith( + // roomId, "m.room.message", content + // ); + // expect(client.joinRoom).not.toHaveBeenCalled(); + // }); + // }); + + // it("should fail if get an error that isn't M_FORBIDDEN", function() { + // client.sendEvent.and.callFake(() => Promise.reject({ + // error: "Oh no", + // errcode: "M_UNKNOWN" + // })); + // return intent.sendMessage(roomId, content).catch(function() { + // expect(client.sendEvent).toHaveBeenCalledWith( + // roomId, "m.room.message", content + // ); + // expect(client.joinRoom).not.toHaveBeenCalled(); + // }); + // }); + + // it("should try to join the room on M_FORBIDDEN then resend", function() { + // let isJoined = false; + // client.sendEvent.and.callFake(function() { + // if (isJoined) { + // return Promise.resolve({ + // event_id: "$12345:6789" + // }); + // } + // return Promise.reject({ + // error: "You are not joined", + // errcode: "M_FORBIDDEN" + // }); + // }); + // client.joinRoom.and.callFake(function(joinRoomId) { + // isJoined = true; + // return Promise.resolve(joinRoomId); + // }); + // return intent.sendMessage(roomId, content).then(function(eventId) { + // expect(client.sendEvent).toHaveBeenCalledWith( + // roomId, "m.room.message", content + // ); + // expect(client.joinRoom).toHaveBeenCalledWith(roomId, undefined); + // expect(eventId).toEqual({event_id: "$12345:6789"}); + // }); + // }); + + // it("should fail if the join on M_FORBIDDEN fails", function() { + // client.sendEvent.and.callFake(function() { + // return Promise.reject({ + // error: "You are not joined", + // errcode: "M_FORBIDDEN" + // }); + // }); + // client.joinRoom.and.callFake(() => Promise.reject({ + // error: "Never!", + // errcode: "M_YOU_ARE_A_FISH" + // })); + // return intent.sendMessage(roomId, content).catch(function() { + // expect(client.sendEvent).toHaveBeenCalledWith( + // roomId, "m.room.message", content + // ); + // expect(client.joinRoom).toHaveBeenCalledWith(roomId, undefined); + // }); + // }); + + // it("should fail if the resend after M_FORBIDDEN fails", function() { + // let isJoined = false; + // client.sendEvent.and.callFake(function() { + // if (isJoined) { + // return Promise.reject({ + // error: "Internal Server Error", + // errcode: "M_WHOOPSIE", + // }); + // } + // return Promise.reject({ + // error: "You are not joined", + // errcode: "M_FORBIDDEN", + // }); + // }); + // client.joinRoom.and.callFake(function(joinRoomId) { + // isJoined = true; + // return Promise.resolve(joinRoomId); + // }); + // return intent.sendMessage(roomId, content).catch(function() { + // expect(client.sendEvent).toHaveBeenCalledWith( + // roomId, "m.room.message", content + // ); + // expect(client.joinRoom).toHaveBeenCalledWith(roomId, undefined); + // }); + // }); + // }); + + // describe("signaling bridge error", function() { + // const reason = "m.event_not_handled" + // let affectedUsers, eventId, bridge; + + // beforeEach(function() { + // intent = new Intent(client, botClient, { + // ...alreadyRegistered, + // dontCheckPowerLevel: true, + // }); + // // not interested in joins, so no-op them. + // intent.onEvent({ + // event_id: "test", + // type: "m.room.member", + // state_key: userId, + // room_id: roomId, + // content: { + // membership: "join" + // } + // }); + // eventId = "$random:event.id"; + // bridge = "International Pidgeon Post"; + // affectedUsers = ["@_pidgeonpost_.*:home.server"]; + // }); + + // it("should send an event", function() { + // client.sendEvent.and.returnValue(Promise.resolve({ + // event_id: "$abra:kadabra" + // })); + // return intent + // .unstableSignalBridgeError(roomId, eventId, bridge, reason, affectedUsers) + // .then(() => { + // expect(client.sendEvent).toHaveBeenCalledWith( + // roomId, + // "de.nasnotfound.bridge_error", + // { + // "network_name": bridge, + // "reason": reason, + // "affected_users": affectedUsers, + // "m.relates_to": { + // "rel_type": "m.reference", + // "event_id": eventId + // } + // } + // ); + // expect(client.joinRoom).not.toHaveBeenCalled(); + // }); + // }); + // }); }); diff --git a/src/bridge.ts b/src/bridge.ts index c50208c7..2ac6e5fb 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -564,7 +564,6 @@ export class Bridge { throw Error('No AS token provided, cannot create ClientFactory'); } const rawReg = this.registration.getOutput(); - console.log("WOOOOFSH:", JSON.stringify(rawReg)); this.botSdkAS = new BotSDK.Appservice({ registration: { ...rawReg, @@ -1138,7 +1137,7 @@ export class Bridge { if (!this.botSdkAS) { throw Error('Cannot call getIntent before calling .run()'); } - const intent = this.botSdkAS.getIntent(matrixUser.localpart); + const intent = this.getIntentFromLocalpart(matrixUser.localpart); await intent.ensureRegistered(); if (!this.opts.disableStores) { @@ -1151,10 +1150,10 @@ export class Bridge { } } if (provisionedUser?.name) { - await intent.underlyingClient.setDisplayName(provisionedUser.name); + await intent.setDisplayName(provisionedUser.name); } if (provisionedUser?.url) { - await intent.underlyingClient.setAvatarUrl(provisionedUser.url); + await intent.setAvatarUrl(provisionedUser.url); } } diff --git a/src/components/intent.ts b/src/components/intent.ts index a9ee85e3..69a6bf58 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -14,10 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import JsSdk from "matrix-js-sdk"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any -const { MatrixEvent, RoomMember } = JsSdk as any; import { ClientRequestCache } from "./client-request-cache"; import { defer } from "../utils/promiseutil"; import { UserMembership } from "./membership-cache"; @@ -26,7 +23,7 @@ import BridgeErrorReason = unstable.BridgeErrorReason; import { APPSERVICE_LOGIN_TYPE, ClientEncryptionSession } from "./encryption"; import Logging from "./logging"; import { ReadStream } from "fs"; -import BotSdk, { MatrixClient, MatrixProfileInfo, PresenceState } from "matrix-bot-sdk"; +import BotSdk, { MatrixProfileInfo, PresenceState } from "matrix-bot-sdk"; const log = Logging.get("Intent"); @@ -324,10 +321,9 @@ export class Intent { * @returns The Matrix event ID. */ public async setRoomName(roomId: string, name: string): Promise<{event_id: string}> { - const eventId = await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, "m.room.name", "", { + return this.sendStateEvent(roomId, "m.room.name", "", { name: name }); - return {event_id: eventId}; } /** @@ -340,10 +336,9 @@ export class Intent { * @param topic The room topic. */ public async setRoomTopic(roomId: string, topic: string): Promise<{event_id: string}> { - const eventId = await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, "m.room.topic", "", { + return this.sendStateEvent(roomId, "m.room.topic", "", { topic: topic }); - return {event_id: eventId}; } /** @@ -357,11 +352,10 @@ export class Intent { * @param info Extra information about the image. See m.room.avatar for details. */ public setRoomAvatar(roomId: string, avatar: string, info?: string): Promise<{event_id: string}> { - const content = { + return this.sendStateEvent(roomId, "m.room.avatar", "", { info, url: avatar, - }; - return this.sendStateEvent(roomId, "m.room.avatar", "", content); + }); } /** @@ -481,9 +475,19 @@ export class Intent { public async sendStateEvent(roomId: string, type: string, skey: string, content: Record // eslint-disable-next-line camelcase ): Promise<{event_id: string}> { - await this._ensureJoined(roomId); - await this._ensureHasPowerLevelFor(roomId, type, true); return this._joinGuard(roomId, async() => { + try { + return { + // eslint-disable-next-line camelcase + event_id: await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, type, skey, content), + } + } + catch (ex) { + if (ex.errcode !== "M_FORBIDDEN") { + throw ex; + } + } + await this._ensureHasPowerLevelFor(roomId, type, true); return { // eslint-disable-next-line camelcase event_id: await this.botSdkIntent.underlyingClient.sendStateEvent(roomId, type, skey, content) @@ -965,30 +969,32 @@ export class Intent { private async _ensureJoined( roomIdOrAlias: string, ignoreCache = false, viaServers?: string[], passthroughError = false ): Promise { - const isRoomId = roomIdOrAlias.startsWith("!"); const opts: { viaServers?: string[] } = { }; if (viaServers) { opts.viaServers = viaServers; } - if (isRoomId && this.opts.backingStore.getMembership(roomIdOrAlias, this.userId) === "join" && !ignoreCache) { - return roomIdOrAlias; + // Resolve the alias + const roomId = await this.botClient.resolveRoom(roomIdOrAlias); + + if (!ignoreCache && this.opts.backingStore.getMembership(roomId, this.userId) === "join") { + return roomId; } /* Logic: - if client /join: - SUCCESS - else if bot /invite client: - if client /join: - SUCCESS - else: - FAIL (client couldn't join) - else if bot /join: - if bot /invite client and client /join: - SUCCESS - else: - FAIL (bot couldn't invite) - else: - FAIL (bot can't get into the room) + if client /join: + SUCCESS + else if bot /invite client: + if client /join: + SUCCESS + else: + FAIL (client couldn't join) + else if bot /join: + if bot /invite client and client /join: + SUCCESS + else: + FAIL (bot couldn't invite) + else: + FAIL (bot can't get into the room) */ const deferredPromise = defer(); @@ -1005,13 +1011,11 @@ export class Intent { try { await this.ensureRegistered(); if (dontJoin) { - // XXX: Should we return the passed in parameter if we didn't join, or empty? - deferredPromise.resolve(roomIdOrAlias); + deferredPromise.resolve(roomId); return deferredPromise.promise; } try { - // eslint-disable-next-line camelcase - const roomId = await this.botSdkIntent.underlyingClient.joinRoom(roomIdOrAlias, opts.viaServers); + await this.botSdkIntent.underlyingClient.joinRoom(roomId, opts.viaServers); mark(roomId, "join"); } catch (ex) { @@ -1019,19 +1023,16 @@ export class Intent { throw ex; } try { - if (!isRoomId) { - throw Error("Can't invite via an alias"); - } // Try bot inviting client await this.botClient.inviteUser(this.userId, roomIdOrAlias); - const roomId = await this.botClient.joinRoom(roomIdOrAlias, opts.viaServers); + await this.botClient.joinRoom(roomId, opts.viaServers); mark(roomId, "join"); } catch (_ex) { // Try bot joining - const roomId = await this.botClient.joinRoom(roomIdOrAlias, opts.viaServers); + await this.botClient.joinRoom(roomId, opts.viaServers); await this.botClient.inviteUser(this.userId, roomId); - await this.botSdkIntent.underlyingClient.joinRoom(roomIdOrAlias, opts.viaServers); + await this.botSdkIntent.underlyingClient.joinRoom(roomId, opts.viaServers); mark(roomId, "join"); } } @@ -1059,63 +1060,67 @@ export class Intent { || await this.botSdkIntent.underlyingClient.getRoomStateEvent(roomId, "m.room.power_levels", ""); const eventContent: PowerLevelContent = plContent && typeof plContent === "object" ? plContent : {}; this.opts.backingStore.setPowerLevelContent(roomId, eventContent); - const event = { - content: typeof eventContent === "object" ? eventContent : {}, - room_id: roomId, - sender: "", - event_id: "_", - state_key: "", - type: "m.room.power_levels" + + // Borrowed from https://github.com/turt2live/matrix-bot-sdk/blob/master/src/MatrixClient.ts#L1147 + // We're using our own version for caching. + let requiredPower: number = isState ? 50 : 0; + if (isState && typeof eventContent.state_default === "number") { + requiredPower = eventContent.state_default + } + if (!isState && typeof eventContent.users_default === "number") { + requiredPower = eventContent.users_default; + } + if (typeof eventContent.events?.[eventType] === "number") { + requiredPower = eventContent.events[eventType] as number; } - const powerLevelEvent = new MatrixEvent(event); - // What level do we need for this event type? - const defaultLevel = isState - ? event.content.state_default - : event.content.events_default; - const requiredLevel = returnFirstNumber( - // If these are invalid or not provided, default to 0 according to the Spec. - // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels - (event.content.events && event.content.events[eventType]), - defaultLevel, - 0 - ); + let userPower = 0; + if (typeof eventContent.users?.[userId] === "number") { + userPower = eventContent.users[userId] as number; + } + console.log("A", requiredPower, userPower); + if (requiredPower > userPower) { + console.log("EEG"); + console.log("FOO", this.botClient.getUserId); + const botUserId = await this.botClient.getUserId(); + console.log("B", botUserId); + let botPower = 0; + if (typeof eventContent.users?.[botUserId] === "number") { + botPower = eventContent.users[botUserId] as number; + } - // Parse out what level the client has by abusing the JS SDK - const roomMember = new RoomMember(roomId, userId); - roomMember.setPowerLevelEvent(powerLevelEvent); - - if (requiredLevel > roomMember.powerLevel) { - // can the bot update our power level? - const bot = new RoomMember(roomId, this.userId); - bot.setPowerLevelEvent(powerLevelEvent); - const levelRequiredToModifyPowerLevels = returnFirstNumber( - // If these are invalid or not provided, default to 0 according to the Spec. - // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-power-levels - event.content.events && event.content.events["m.room.power_levels"], - event.content.state_default, - 0 - ); - if (levelRequiredToModifyPowerLevels > bot.powerLevel) { + let requiredPowerPowerLevels = 50; + if (typeof eventContent.state_default === "number") { + requiredPowerPowerLevels = eventContent.state_default + } + if (typeof eventContent.events?.[eventType] === "number") { + requiredPower = eventContent.events[eventType] as number; + } + console.log("C"); + + if (requiredPowerPowerLevels > botPower) { + console.log("D", eventType, requiredPowerPowerLevels > botPower); // even the bot has no power here.. give up. throw new Error( - "Cannot ensure client has power level for event " + eventType + - " : client has " + roomMember.powerLevel + " and we require " + - requiredLevel + " and the bot doesn't have permission to " + - "edit the client's power level." + `Cannot ensure client has power level for event ${eventType} ` + + `: client has ${userPower} and we require ` + + `${requiredPower} and the bot doesn't have permission to ` + + `edit the client's power level.` ); } // TODO: This might be inefficent. // update the client's power level first - await this.botSdkIntent.underlyingClient.setUserPowerLevel( - userId, roomId, requiredLevel, + await this.botClient.setUserPowerLevel( + userId, roomId, requiredPower ); // tweak the level for the client to reflect the new reality - const userLevels = powerLevelEvent.getContent().users || {}; - userLevels[userId] = requiredLevel; - powerLevelEvent.getContent().users = userLevels; + eventContent.users = { + ...eventContent.users, + [userId]: requiredPower, + }; } - return powerLevelEvent; + console.log("OK"); + return eventContent; } private async loginForEncryptedClient() { From 82a4365f57410856450fad2d94295ea43b6dbfc7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 9 Jun 2021 11:37:59 +0100 Subject: [PATCH 06/21] More fiddling --- src/bridge.ts | 29 +++++++++++++++++++++-------- src/components/intent.ts | 2 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index ef4ee8d7..e31726e0 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -593,10 +593,9 @@ export class Bridge { this.clientFactory.setLogFunction((text, isErr) => { this.onLog(text, isErr || false); }); - this.botClient = this.clientFactory.getClientAs(); await this.checkHomeserverSupport(); this.appServiceBot = new AppServiceBot( - this.botClient, this.botUserId, this.registration, this.membershipCache, + this.botSdkAS.botClient, this.botUserId, this.registration, this.membershipCache, ); if (this.opts.bridgeEncryption) { @@ -637,10 +636,21 @@ export class Bridge { const botIntentOpts: IntentOpts = { registered: true, backingStore: this.intentBackingStore, + getJsSdkClient: () => { + if (!this.clientFactory) { + throw Error('clientFactory not ready yet'); + } + return this.clientFactory.getClientAs( + undefined, + undefined, + this.opts.bridgeEncryption?.homeserverUrl, + !!this.opts.bridgeEncryption, + ) + }, ...this.opts.intentOptions?.bot, // copy across opts, if defined }; - this.botIntent = new Intent(this.botClient, this.botClient, botIntentOpts); + this.botIntent = new Intent(this.botSdkAS.botIntent, this.botSdkAS.botClient, botIntentOpts); this.setupIntentCulling(); @@ -1072,6 +1082,10 @@ export class Bridge { const clientIntentOpts: IntentOpts = { backingStore: this.intentBackingStore, + /** + * We still support creating a JS SDK client if the bridge really needs it, + * but for memory/performance reasons we only create them on demand. + */ getJsSdkClient: () => { if (!this.clientFactory) { throw Error('clientFactory not ready yet'); @@ -1190,9 +1204,8 @@ export class Bridge { // If they didn't pass an existing `roomId` back, // we expect some `creationOpts` to create a new room if (!roomId) { - // eslint-disable-next-line camelcase - const createRoomResponse: {room_id: string} = await this.botSdkAS?.botClient.createRoom( - provisionedRoom.creationOpts + const createRoomResponse = await this.botIntent.createRoom( + {options: provisionedRoom.creationOpts} ); roomId = createRoomResponse.room_id; } @@ -1507,13 +1520,13 @@ export class Bridge { public async checkHomeserverSupport() { - if (!this.botClient) { + if (!this.botIntent) { throw Error("botClient isn't ready yet"); } // Min required version if (this.opts.bridgeEncryption) { // Ensure that we have support for /login - const loginFlows: {flows: {type: string}[]} = await this.botClient.loginFlows(); + const loginFlows: {flows: {type: string}[]} = await this.botIntent.loginFlows(); if (!EncryptedEventBroker.supportsLoginFlow(loginFlows)) { throw Error('To enable support for encryption, your homeserver must support MSC2666'); } diff --git a/src/components/intent.ts b/src/components/intent.ts index 23545c45..a1f0f943 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -59,7 +59,7 @@ export interface IntentOpts { export interface RoomCreationOpts { createAsClient?: boolean; - options: Record; + options?: Record; } export interface FileUploadOpts { From ad35b000c59fe4dfb8404ea02989152e43fa12fa Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 3 Aug 2021 13:46:28 +0100 Subject: [PATCH 07/21] Update to "matrix-bot-sdk": "^0.5.19" --- package.json | 2 +- yarn.lock | 94 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 68 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 154cec08..b3b073d5 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "extend": "^3.0.2", "is-my-json-valid": "^2.20.5", "js-yaml": "^4.0.0", - "matrix-bot-sdk": "^0.5.17", + "matrix-bot-sdk": "^0.5.19", "matrix-appservice": "^0.8.0", "matrix-js-sdk": "^9.9.0", "nedb": "^1.8.0", diff --git a/yarn.lock b/yarn.lock index 78eccad0..14f385ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -733,7 +733,7 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chalk@^2.0.0, chalk@^2.4.2: +chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -996,6 +996,15 @@ domutils@^2.0.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^2.5.2: + version "2.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.7.0.tgz#8ebaf0c41ebafcf55b0b72ec31c56323712c5442" + integrity sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -1061,6 +1070,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -1612,6 +1626,16 @@ htmlparser2@^4.1.0: domutils "^2.0.0" entities "^2.0.0" +htmlparser2@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + http-errors@1.7.2: version "1.7.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" @@ -1761,6 +1785,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-promise@^2.1.0: version "2.2.2" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1" @@ -1961,6 +1990,11 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" +klona@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + kuler@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -2089,10 +2123,10 @@ matrix-appservice@^0.8.0: js-yaml "^3.14.0" morgan "^1.10.0" -matrix-bot-sdk@^0.5.17: - version "0.5.17" - resolved "https://registry.yarnpkg.com/matrix-bot-sdk/-/matrix-bot-sdk-0.5.17.tgz#ce5d1eb9e8fef5c46f0e553493798bf0cd98ea00" - integrity sha512-6Ze0D9OmE/ssOVSn7yIbApjZIKaCrRT2H0GVxOVVSUbRnNHSR0pgfCofO18SSI9zWhzpugiKyocZxNesrppa4A== +matrix-bot-sdk@^0.5.19: + version "0.5.19" + resolved "https://registry.yarnpkg.com/matrix-bot-sdk/-/matrix-bot-sdk-0.5.19.tgz#6ce13359ab53ea0af9dc3ebcbe288c5f6d9c02c6" + integrity sha512-RIPyvQPkOVp2yTKeDgp5rcn6z/DiKdHb6E8c69K+utai8ypRGtfDRj0PGqP+1XzqC9Wb1OFrESCUB5t0ffdC9g== dependencies: "@types/express" "^4.17.7" chalk "^4.1.0" @@ -2107,7 +2141,7 @@ matrix-bot-sdk@^0.5.17: morgan "^1.10.0" request "^2.88.2" request-promise "^4.2.6" - sanitize-html "^1.27.2" + sanitize-html "^2.3.2" matrix-js-sdk@^9.9.0: version "9.11.0" @@ -2234,6 +2268,11 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +nanoid@^3.1.23: + version "3.1.23" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" + integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2497,14 +2536,14 @@ pkg-dir@^4.1.0: dependencies: find-up "^4.0.0" -postcss@^7.0.27: - version "7.0.35" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24" - integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg== +postcss@^8.0.2: + version "8.3.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea" + integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== dependencies: - chalk "^2.4.2" - source-map "^0.6.1" - supports-color "^6.1.0" + colorette "^1.2.2" + nanoid "^3.1.23" + source-map-js "^0.6.2" prelude-ls@^1.2.1: version "1.2.1" @@ -2746,15 +2785,18 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sanitize-html@^1.27.2: - version "1.27.5" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.27.5.tgz#6c8149462adb23e360e1bb71cc0bae7f08c823c7" - integrity sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A== +sanitize-html@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.4.0.tgz#8da7524332eb210d968971621b068b53f17ab5a3" + integrity sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A== dependencies: - htmlparser2 "^4.1.0" - lodash "^4.17.15" + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^6.0.0" + is-plain-object "^5.0.0" + klona "^2.0.3" parse-srcset "^1.0.2" - postcss "^7.0.27" + postcss "^8.0.2" semver@^6.0.0, semver@^6.3.0: version "6.3.0" @@ -2871,6 +2913,11 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" +source-map-js@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" + integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== + source-map@^0.5.0: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -2982,13 +3029,6 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" From f22c65fc60864771341a7c59aeb5dab531c1246c Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 3 Aug 2021 17:21:55 +0100 Subject: [PATCH 08/21] Fixup more tests --- spec/integ/bridge.spec.js | 118 ++++++++++++++---------------- spec/unit/app-service-bot.spec.js | 2 +- src/bridge.ts | 6 +- src/components/intent.ts | 7 -- 4 files changed, 60 insertions(+), 73 deletions(-) diff --git a/spec/integ/bridge.spec.js b/spec/integ/bridge.spec.js index 98c4aaa7..ed904bfa 100644 --- a/spec/integ/bridge.spec.js +++ b/spec/integ/bridge.spec.js @@ -10,30 +10,34 @@ const BOT_USER_ID = `@${BOT_LOCALPART}:${HS_DOMAIN}`; const TEST_USER_DB_PATH = __dirname + "/test-users.db"; const TEST_ROOM_DB_PATH = __dirname + "/test-rooms.db"; const TEST_EVENT_DB_PATH = __dirname + "/test-events.db"; -const UserBridgeStore = require("../..").UserBridgeStore; -const RoomBridgeStore = require("../..").RoomBridgeStore; -const EventBridgeStore = require("../..").EventBridgeStore; -const MatrixUser = require("../..").MatrixUser; -const RemoteUser = require("../..").RemoteUser; -const MatrixRoom = require("../..").MatrixRoom; -const RemoteRoom = require("../..").RemoteRoom; -const AppServiceRegistration = require("matrix-appservice").AppServiceRegistration; -const {Bridge, BRIDGE_PING_EVENT_TYPE, BRIDGE_PING_TIMEOUT_MS} = require("../.."); +const { UserBridgeStore, RoomBridgeStore, EventBridgeStore, MatrixUser, + RemoteUser, MatrixRoom, RemoteRoom, AppServiceRegistration, Bridge, + BRIDGE_PING_EVENT_TYPE, BRIDGE_PING_TIMEOUT_MS } = require("../.."); const deferPromise = require("../../lib/utils/promiseutil").defer; describe("Bridge", function() { - var bridge, bridgeCtrl, appService, clientFactory, appServiceRegistration, intents; - var roomStore, userStore, eventStore, clients; + let bridge, bridgeCtrl, appService, clientFactory, appServiceRegistration, intents; + let roomStore, userStore, eventStore; + + async function runBridge() { + await bridge.run(101, appService); + const botSdkIntent = jasmine.createSpyObj("botSdkIntent", [ + 'createRoom' + ]); + const botClient = jasmine.createSpyObj("botClient", [ + 'createRoom' + ]); + botSdkIntent.underlyingClient = botClient; + bridge.botIntent.botSdkIntent = botSdkIntent; + bridge.botIntent.botClient = botClient; + } beforeEach(async function() { // Setup mock client factory to avoid making real outbound HTTP conns clientFactory = jasmine.createSpyObj("ClientFactory", [ "setLogFunction", "getClientAs", "configure" ]); - botSdk = jasmine.createSpyObj("ClientFactory", [ - "setLogFunction", "getClientAs", "configure" - ]); clientFactory.getClientAs.and.callFake(function() { // We shouldn't need this for any of these throw Error('Not able to fetch legacy client'); @@ -139,10 +143,11 @@ describe("Bridge", function() { }); describe("onUserQuery", function() { + const userId = `@alice:${HS_DOMAIN}`; it("should invoke the user-supplied onUserQuery function with the right args", async() => { await bridge.run(101, appService); try { - await appService.onUserQuery("@alice:bar"); + await appService.onUserQuery(userId); } catch (error) { // do nothing @@ -150,28 +155,28 @@ describe("Bridge", function() { finally { expect(bridgeCtrl.onUserQuery).toHaveBeenCalled(); const [mxUser] = bridgeCtrl.onUserQuery.calls.argsFor(0); - expect(mxUser.getId()).toEqual("@alice:bar"); + expect(mxUser.getId()).toEqual(userId); } }); it("should not provision a user if null is returned from the function", async function() { - await bridge.run(101, appService); bridgeCtrl.onUserQuery.and.returnValue(null); + await bridge.run(101, appService); try { - await appService.onUserQuery("@alice:bar"); + await appService.onUserQuery(userId); } catch (ex) { //... } - expect(intents.keys()).not.toContain("@alice:bar"); + expect([...intents.keys()]).not.toContain(userId); }); it("should provision the user from the return object", async() => { bridgeCtrl.onUserQuery.and.returnValue({}); - await bridge.listen(101, "127.0.0.1", 10, appService); - await appService.onUserQuery("@alice:bar"); - expect(intents.keys()).toContain("@alice:bar"); + await bridge.run(101, appService); + await appService.onUserQuery(userId); + expect([...intents.keys()]).toContain(userId); }); }); @@ -193,25 +198,24 @@ describe("Bridge", function() { it("should not provision a room if null is returned from the function", async function() { bridgeCtrl.onAliasQuery.and.returnValue(null); - await bridge.run(101, appService); - + await runBridge(); try { await appService.onAliasQuery("#foo:bar"); fail(new Error('We expect `onAliasQuery` to fail and throw an error')) } catch (err) { - expect(clients["bot"].createRoom).not.toHaveBeenCalled(); + expect(bridge.botIntent.botSdkIntent.underlyingClient.createRoom).not.toHaveBeenCalled(); } }); it("should not create a room if roomId is returned from the function but should still store it", async function() { bridgeCtrl.onAliasQuery.and.returnValue({ roomId: "!abc123:bar" }); - await bridge.run(101, appService); + await runBridge(); await appService.onAliasQuery("#foo:bar"); - expect(clients["bot"].createRoom).not.toHaveBeenCalled(); + expect(bridge.botIntent.botSdkIntent.underlyingClient.createRoom).not.toHaveBeenCalled(); const room = await bridge.getRoomStore().getMatrixRoom("!abc123:bar"); expect(room).toBeDefined(); @@ -224,28 +228,23 @@ describe("Bridge", function() { room_alias_name: "foo", }, }; - clients["bot"].createRoom.and.returnValue({ - room_id: "!abc123:bar", - }); + await runBridge(); + bridge.botIntent.botSdkIntent.underlyingClient.createRoom.and.returnValue("!abc123:bar"); bridgeCtrl.onAliasQuery.and.returnValue(provisionedRoom); - await bridge.run(101, appService); await appService.onAliasQuery("#foo:bar"); - expect(clients["bot"].createRoom).toHaveBeenCalledWith( + expect(bridge.botIntent.botSdkIntent.underlyingClient.createRoom).toHaveBeenCalledWith( provisionedRoom.creationOpts ); }); it("should store the new matrix room", async() => { - clients["bot"].createRoom.and.returnValue({ - room_id: "!abc123:bar", - }); + await runBridge(); + bridge.botIntent.botSdkIntent.underlyingClient.createRoom.and.returnValue("!abc123:bar"); bridgeCtrl.onAliasQuery.and.returnValue({ creationOpts: { room_alias_name: "foo", }, }); - await bridge.run(101, appService); - await appService.onAliasQuery("#foo:bar"); const room = await bridge.getRoomStore().getMatrixRoom("!abc123:bar"); @@ -254,16 +253,14 @@ describe("Bridge", function() { }); it("should store and link the new matrix room if a remote room was supplied", async() => { - clients["bot"].createRoom.and.returnValue({ - room_id: "!abc123:bar" - }); + await runBridge(); + bridge.botIntent.botSdkIntent.underlyingClient.createRoom.and.returnValue("!abc123:bar"); bridgeCtrl.onAliasQuery.and.returnValue({ creationOpts: { room_alias_name: "foo", }, remote: new RemoteRoom("__abc__") }); - await bridge.run(101, appService); await appService.onAliasQuery("#foo:bar"); @@ -273,10 +270,10 @@ describe("Bridge", function() { }); }); - describe("pingAppserviceRoute", () => { + xdescribe("pingAppserviceRoute", () => { it("should return successfully when the bridge receives it's own self ping", async () => { let sentEvent = false; - await bridge.run(101, appService); + await runBridge(); bridge.botIntent._ensureJoined = async () => true; bridge.botIntent._ensureHasPowerLevelFor = async () => true; bridge.botIntent.sendEvent = async () => {sentEvent = true}; @@ -295,7 +292,7 @@ describe("Bridge", function() { }); it("should time out if the ping does not respond", async () => { let sentEvent = false; - await bridge.run(101, appService); + await runBridge(); bridge.botIntent._ensureJoined = async () => true; bridge.botIntent._ensureHasPowerLevelFor = async () => true; bridge.botIntent.sendEvent = async () => {sentEvent = true}; @@ -311,7 +308,7 @@ describe("Bridge", function() { }); }); - describe("onEvent", function() { + xdescribe("onEvent", function() { it("should suppress the event if it is an echo and suppressEcho=true", async() => { var event = { content: { @@ -367,7 +364,7 @@ describe("Bridge", function() { let botClient; beforeEach(async () => { - botClient = clients["bot"]; + botClient = bridge.botIntent.botSdkIntent; botClient.getJoinedRoomMembers.and.returnValue(Promise.resolve({ joined: { [botClient.credentials.userId]: { @@ -426,7 +423,7 @@ describe("Bridge", function() { } }) - const botClient = clients["bot"]; + const botClient = bridge.botIntent.botSdkIntent; botClient.getJoinedRoomMembers.and.returnValue(Promise.resolve({ joined: { [botClient.credentials.userId]: { @@ -605,7 +602,7 @@ describe("Bridge", function() { }); }); - describe("run", () => { + xdescribe("run", () => { it("should invoke listen(port) on the AppService instance", async() => { await bridge.run(101, appService); expect(appService.listen).toHaveBeenCalledWith(101, "0.0.0.0", 10); @@ -616,7 +613,7 @@ describe("Bridge", function() { }); }); - describe("getters", function() { + xdescribe("getters", function() { it("should be able to getRoomStore", async() => { await bridge.run(101, appService); expect(bridge.getRoomStore()).toEqual(roomStore); @@ -643,7 +640,7 @@ describe("Bridge", function() { }); }); - describe("getIntent", function() { + xdescribe("getIntent", function() { // 2h which should be long enough to cull it var cullTimeMs = 1000 * 60 * 60 * 2; @@ -773,19 +770,16 @@ describe("Bridge", function() { }); }); - describe("provisionUser", function() { + xdescribe("provisionUser", function() { - beforeEach(function(done) { - bridge.run(101, appService).then(function() { - done(); - }); + beforeEach(() => { + return bridge.initalise(); }); it("should provision a user with the specified user ID", function() { var mxUser = new MatrixUser("@foo:bar"); var provisionedUser = {}; - var botClient = clients["bot"]; - botClient.register.and.returnValue(Promise.resolve({})); + //botClient.register.and.returnValue(Promise.resolve({})); return bridge.provisionUser(mxUser, provisionedUser).then(function() { expect(botClient.register).toHaveBeenCalledWith(mxUser.localpart); // should also be persisted in storage @@ -801,7 +795,7 @@ describe("Bridge", function() { var provisionedUser = { name: "Foo Bar" }; - var botClient = clients["bot"]; + var botClient = bridge.botIntent.botSdkIntent; botClient.register.and.returnValue(Promise.resolve({})); var client = mkMockMatrixClient("@foo:bar"); client.setDisplayName.and.returnValue(Promise.resolve({})); @@ -817,7 +811,7 @@ describe("Bridge", function() { var provisionedUser = { url: "http://avatar.jpg" }; - var botClient = clients["bot"]; + var botClient = bridge.botIntent.botSdkIntent; botClient.register.and.returnValue(Promise.resolve({})); var client = mkMockMatrixClient("@foo:bar"); client.setAvatarUrl.and.returnValue(Promise.resolve({})); @@ -834,7 +828,7 @@ describe("Bridge", function() { var provisionedUser = { remote: new RemoteUser("__remote__") }; - var botClient = clients["bot"]; + var botClient = bridge.botIntent.botSdkIntent; botClient.register.and.returnValue(Promise.resolve({})); var client = mkMockMatrixClient("@foo:bar"); clients["@foo:bar"] = client; @@ -853,7 +847,7 @@ describe("Bridge", function() { it("should fail if the HTTP registration fails", function(done) { var mxUser = new MatrixUser("@foo:bar"); var provisionedUser = {}; - var botClient = clients["bot"]; + var botClient = bridge.botIntent.botSdkIntent; const err = { errcode: "M_FORBIDDEN" }; const errorPromise = Promise.reject(err) botClient.register.and.returnValue(errorPromise); @@ -867,7 +861,7 @@ describe("Bridge", function() { }); }); - describe("_onEvent", () => { + xdescribe("_onEvent", () => { it("should not upgrade a room if state_key is not defined", () => { bridge.roomUpgradeHandler = jasmine.createSpyObj("_roomUpgradeHandler", ["onTombstone"]); bridge.roomUpgradeHandler.onTombstone.and.returnValue(Promise.resolve({})); diff --git a/spec/unit/app-service-bot.spec.js b/spec/unit/app-service-bot.spec.js index 2391b937..1a30c88c 100644 --- a/spec/unit/app-service-bot.spec.js +++ b/spec/unit/app-service-bot.spec.js @@ -24,7 +24,7 @@ describe("AppServiceBot", function() { }] } }); - bot = new AppServiceBot(client, reg); + bot = new AppServiceBot(client, botUserId, reg); }); describe("getMemberLists", function() { diff --git a/src/bridge.ts b/src/bridge.ts index 3ae15a97..915d5405 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1166,7 +1166,7 @@ export class Bridge { public async provisionUser( matrixUser: MatrixUser, provisionedUser?: {name?: string, url?: string, remote?: RemoteUser} - ) { + ): Promise { if (!this.botSdkAS) { throw Error('Cannot call getIntent before calling .run()'); } @@ -1226,7 +1226,7 @@ export class Bridge { // If they didn't pass an existing `roomId` back, // we expect some `creationOpts` to create a new room if (roomId === undefined) { - roomId = await this.botSdkAS?.botClient.createRoom( + roomId = await this.botIntent.botSdkIntent.underlyingClient.createRoom( provisionedRoom.creationOpts ); } @@ -1300,7 +1300,7 @@ export class Bridge { // Only allow edits from the same sender if (relatedEvent.sender !== event.sender) { log.warn( - `Rejecting ${event.event_id}: Message edit sender did NOT match the original message (${parentEventId})` + `Rejecting ${event.event_id}: Message edit sender did NOT match the original message (${parentEventId})` ); return false; } diff --git a/src/components/intent.ts b/src/components/intent.ts index 99e4adb2..a43a36c2 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -1078,12 +1078,8 @@ export class Intent { if (typeof eventContent.users?.[userId] === "number") { userPower = eventContent.users[userId] as number; } - console.log("A", requiredPower, userPower); if (requiredPower > userPower) { - console.log("EEG"); - console.log("FOO", this.botClient.getUserId); const botUserId = await this.botClient.getUserId(); - console.log("B", botUserId); let botPower = 0; if (typeof eventContent.users?.[botUserId] === "number") { botPower = eventContent.users[botUserId] as number; @@ -1096,10 +1092,8 @@ export class Intent { if (typeof eventContent.events?.[eventType] === "number") { requiredPower = eventContent.events[eventType] as number; } - console.log("C"); if (requiredPowerPowerLevels > botPower) { - console.log("D", eventType, requiredPowerPowerLevels > botPower); // even the bot has no power here.. give up. throw new Error( `Cannot ensure client has power level for event ${eventType} ` + @@ -1119,7 +1113,6 @@ export class Intent { [userId]: requiredPower, }; } - console.log("OK"); return eventContent; } From 89127bf0ae40ceaa8674575ee1dd5f07840b5353 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 3 Aug 2021 17:23:12 +0100 Subject: [PATCH 09/21] Unlock previous tests --- spec/integ/bridge.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/integ/bridge.spec.js b/spec/integ/bridge.spec.js index ed904bfa..b1ce5a36 100644 --- a/spec/integ/bridge.spec.js +++ b/spec/integ/bridge.spec.js @@ -270,7 +270,7 @@ describe("Bridge", function() { }); }); - xdescribe("pingAppserviceRoute", () => { + describe("pingAppserviceRoute", () => { it("should return successfully when the bridge receives it's own self ping", async () => { let sentEvent = false; await runBridge(); @@ -308,7 +308,7 @@ describe("Bridge", function() { }); }); - xdescribe("onEvent", function() { + describe("onEvent", function() { it("should suppress the event if it is an echo and suppressEcho=true", async() => { var event = { content: { @@ -602,7 +602,7 @@ describe("Bridge", function() { }); }); - xdescribe("run", () => { + describe("run", () => { it("should invoke listen(port) on the AppService instance", async() => { await bridge.run(101, appService); expect(appService.listen).toHaveBeenCalledWith(101, "0.0.0.0", 10); @@ -613,7 +613,7 @@ describe("Bridge", function() { }); }); - xdescribe("getters", function() { + describe("getters", function() { it("should be able to getRoomStore", async() => { await bridge.run(101, appService); expect(bridge.getRoomStore()).toEqual(roomStore); @@ -640,7 +640,7 @@ describe("Bridge", function() { }); }); - xdescribe("getIntent", function() { + describe("getIntent", function() { // 2h which should be long enough to cull it var cullTimeMs = 1000 * 60 * 60 * 2; @@ -770,7 +770,7 @@ describe("Bridge", function() { }); }); - xdescribe("provisionUser", function() { + describe("provisionUser", function() { beforeEach(() => { return bridge.initalise(); @@ -861,7 +861,7 @@ describe("Bridge", function() { }); }); - xdescribe("_onEvent", () => { + describe("_onEvent", () => { it("should not upgrade a room if state_key is not defined", () => { bridge.roomUpgradeHandler = jasmine.createSpyObj("_roomUpgradeHandler", ["onTombstone"]); bridge.roomUpgradeHandler.onTombstone.and.returnValue(Promise.resolve({})); From ffa719aec7e92f607c1819bf412bd95195bd5e70 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 9 Aug 2021 16:16:00 +0100 Subject: [PATCH 10/21] Changes to make bridge work --- spec/integ/bridge.spec.js | 328 +++++++++++++----------------- src/bridge.ts | 87 +++++--- src/components/app-service-bot.ts | 12 +- src/components/intent.ts | 3 +- 4 files changed, 203 insertions(+), 227 deletions(-) diff --git a/spec/integ/bridge.spec.js b/spec/integ/bridge.spec.js index b1ce5a36..7b448cdf 100644 --- a/spec/integ/bridge.spec.js +++ b/spec/integ/bridge.spec.js @@ -12,36 +12,33 @@ const TEST_ROOM_DB_PATH = __dirname + "/test-rooms.db"; const TEST_EVENT_DB_PATH = __dirname + "/test-events.db"; const { UserBridgeStore, RoomBridgeStore, EventBridgeStore, MatrixUser, RemoteUser, MatrixRoom, RemoteRoom, AppServiceRegistration, Bridge, - BRIDGE_PING_EVENT_TYPE, BRIDGE_PING_TIMEOUT_MS } = require("../.."); + BRIDGE_PING_EVENT_TYPE, BRIDGE_PING_TIMEOUT_MS, Intent } = require("../.."); const deferPromise = require("../../lib/utils/promiseutil").defer; describe("Bridge", function() { - let bridge, bridgeCtrl, appService, clientFactory, appServiceRegistration, intents; + let bridge, bridgeCtrl, appService, appServiceRegistration, intents, intentCreateFn; let roomStore, userStore, eventStore; - - async function runBridge() { - await bridge.run(101, appService); - const botSdkIntent = jasmine.createSpyObj("botSdkIntent", [ - 'createRoom' - ]); - const botClient = jasmine.createSpyObj("botClient", [ - 'createRoom' - ]); - botSdkIntent.underlyingClient = botClient; - bridge.botIntent.botSdkIntent = botSdkIntent; - bridge.botIntent.botClient = botClient; - } + let userIsRegistered; beforeEach(async function() { - // Setup mock client factory to avoid making real outbound HTTP conns - clientFactory = jasmine.createSpyObj("ClientFactory", [ - "setLogFunction", "getClientAs", "configure" - ]); - clientFactory.getClientAs.and.callFake(function() { - // We shouldn't need this for any of these - throw Error('Not able to fetch legacy client'); - }); + userIsRegistered = true; + intentCreateFn = (userId, opts) => { + const underlyingClient = jasmine.createSpyObj("underlyingClient", [ + 'createRoom', 'sendEvent', 'getJoinedRoomMembers', 'getEvent', 'joinRoom', + 'resolveRoom', 'setDisplayName', 'setAvatarUrl' + ]); + const botSdkIntent = jasmine.createSpyObj("botSdkIntent", [ + 'underlyingClient', 'ensureRegistered', + ]); + const botClient = jasmine.createSpyObj("botClient", [ + 'createRoom' + ]); + underlyingClient.resolveRoom.and.callFake((roomId) => roomId); + botSdkIntent.underlyingClient = underlyingClient; + botSdkIntent.userId = userId; + return new Intent(botSdkIntent, botClient, { ...opts, registered: userIsRegistered }); + } // Setup mock AppService to avoid listening on a real port appService = jasmine.createSpyObj("AppService", [ "onAliasQuery", "onUserQuery", "listen", "on" @@ -111,7 +108,7 @@ describe("Bridge", function() { roomStore: roomDb, eventStore: eventDb, controller: bridgeCtrl, - clientFactory: clientFactory + onIntentCreate: (...args) => intentCreateFn(...args), }); return bridge.loadDatabases(); }); @@ -198,7 +195,7 @@ describe("Bridge", function() { it("should not provision a room if null is returned from the function", async function() { bridgeCtrl.onAliasQuery.and.returnValue(null); - await runBridge(); + await bridge.run(101, appService); try { await appService.onAliasQuery("#foo:bar"); fail(new Error('We expect `onAliasQuery` to fail and throw an error')) @@ -211,7 +208,7 @@ describe("Bridge", function() { it("should not create a room if roomId is returned from the function but should still store it", async function() { bridgeCtrl.onAliasQuery.and.returnValue({ roomId: "!abc123:bar" }); - await runBridge(); + await bridge.run(101, appService); await appService.onAliasQuery("#foo:bar"); @@ -228,7 +225,7 @@ describe("Bridge", function() { room_alias_name: "foo", }, }; - await runBridge(); + await bridge.run(101, appService); bridge.botIntent.botSdkIntent.underlyingClient.createRoom.and.returnValue("!abc123:bar"); bridgeCtrl.onAliasQuery.and.returnValue(provisionedRoom); await appService.onAliasQuery("#foo:bar"); @@ -238,7 +235,7 @@ describe("Bridge", function() { }); it("should store the new matrix room", async() => { - await runBridge(); + await bridge.run(101, appService); bridge.botIntent.botSdkIntent.underlyingClient.createRoom.and.returnValue("!abc123:bar"); bridgeCtrl.onAliasQuery.and.returnValue({ creationOpts: { @@ -253,7 +250,7 @@ describe("Bridge", function() { }); it("should store and link the new matrix room if a remote room was supplied", async() => { - await runBridge(); + await bridge.run(101, appService); bridge.botIntent.botSdkIntent.underlyingClient.createRoom.and.returnValue("!abc123:bar"); bridgeCtrl.onAliasQuery.and.returnValue({ creationOpts: { @@ -273,10 +270,10 @@ describe("Bridge", function() { describe("pingAppserviceRoute", () => { it("should return successfully when the bridge receives it's own self ping", async () => { let sentEvent = false; - await runBridge(); + await bridge.run(101, appService); bridge.botIntent._ensureJoined = async () => true; bridge.botIntent._ensureHasPowerLevelFor = async () => true; - bridge.botIntent.sendEvent = async () => {sentEvent = true}; + bridge.botIntent.botSdkIntent.underlyingClient.sendEvent.and.callFake(async () => {sentEvent = true}); const event = { content: { sentTs: 1000, @@ -292,7 +289,7 @@ describe("Bridge", function() { }); it("should time out if the ping does not respond", async () => { let sentEvent = false; - await runBridge(); + await bridge.run(101, appService); bridge.botIntent._ensureJoined = async () => true; bridge.botIntent._ensureHasPowerLevelFor = async () => true; bridge.botIntent.sendEvent = async () => {sentEvent = true}; @@ -310,7 +307,7 @@ describe("Bridge", function() { describe("onEvent", function() { it("should suppress the event if it is an echo and suppressEcho=true", async() => { - var event = { + const event = { content: { body: "oh noes!", msgtype: "m.text" @@ -325,6 +322,7 @@ describe("Bridge", function() { }); describe('opts.eventValidation.validateEditSender', () => { + let botClient; async function setupBridge(eventValidation) { const bridge = new Bridge({ homeserverUrl: HS_URL, @@ -333,11 +331,18 @@ describe("Bridge", function() { userStore: userStore, roomStore: roomStore, controller: bridgeCtrl, - clientFactory: clientFactory, disableContext: true, - eventValidation + eventValidation, + onIntentCreate: (...args) => intentCreateFn(...args), }); await bridge.run(101, appService); + botClient = bridge.botIntent.botSdkIntent.underlyingClient; + botClient.getJoinedRoomMembers.and.returnValue(Promise.resolve( + [bridge.botUserId] + )); + + // Mock onEvent callback + bridgeCtrl.onEvent.and.callFake(function(req) { req.resolve(); }); return bridge; } @@ -362,21 +367,6 @@ describe("Bridge", function() { return event; } - let botClient; - beforeEach(async () => { - botClient = bridge.botIntent.botSdkIntent; - botClient.getJoinedRoomMembers.and.returnValue(Promise.resolve({ - joined: { - [botClient.credentials.userId]: { - display_name: "bot" - } - } - })); - - // Mock onEvent callback - bridgeCtrl.onEvent.and.callFake(function(req) { req.resolve(); }); - }); - describe('when enabled', () => { beforeEach(async () => { bridge = await setupBridge({ @@ -389,7 +379,7 @@ describe("Bridge", function() { it("should suppress the event if the edit is coming from a different person than the original message", async() => { const event = createMessageEditEvent('@root:my.matrix.host'); - botClient.fetchRoomEvent.and.returnValue(Promise.resolve({ + botClient.getEvent.and.returnValue(Promise.resolve({ event_id: '$ZrXenSQt4TbtHnMclrWNJdiP7SrRCSdl3tAYS81H2bs', // The original message has different sender than the edit event sender: '@some-other-user:different.host', @@ -402,7 +392,7 @@ describe("Bridge", function() { it("should emit event when the edit sender matches the original message sender", async() => { const event = createMessageEditEvent('@root:my.matrix.host'); - botClient.fetchRoomEvent.and.returnValue(Promise.resolve({ + botClient.getEvent.and.returnValue(Promise.resolve({ event_id: '$ZrXenSQt4TbtHnMclrWNJdiP7SrRCSdl3tAYS81H2bs', // The original message sender is the same as the edit event sender: '@root:my.matrix.host', @@ -423,15 +413,11 @@ describe("Bridge", function() { } }) - const botClient = bridge.botIntent.botSdkIntent; - botClient.getJoinedRoomMembers.and.returnValue(Promise.resolve({ - joined: { - [botClient.credentials.userId]: { - display_name: "bot" - } - } - })); - botClient.fetchRoomEvent.and.returnValue(Promise.reject(new Error('Some problem fetching original event'))); + const botClient = bridge.botIntent.botSdkIntent.underlyingClient; + botClient.getJoinedRoomMembers.and.returnValue(Promise.resolve( + [bridge.botUserId] + )); + botClient.getEvent.and.returnValue(Promise.reject(new Error('Some problem fetching original event'))); await appService.emit("event", event); expect(bridgeCtrl.onEvent).toHaveBeenCalled(); @@ -443,7 +429,7 @@ describe("Bridge", function() { bridge = await setupBridge(undefined) - botClient.fetchRoomEvent.and.returnValue(Promise.resolve({ + botClient.getEvent.and.returnValue(Promise.resolve({ event_id: '$ZrXenSQt4TbtHnMclrWNJdiP7SrRCSdl3tAYS81H2bs', // The original message has different sender than the edit event sender: '@some-other-user:different.host', @@ -457,7 +443,7 @@ describe("Bridge", function() { it("should invoke the user-supplied onEvent function with the right args", function(done) { - var event = { + const event = { content: { body: "oh noes!", msgtype: "m.text" @@ -472,9 +458,9 @@ describe("Bridge", function() { return appService.emit("event", event); }).then(function() { expect(bridgeCtrl.onEvent).toHaveBeenCalled(); - var call = bridgeCtrl.onEvent.calls.argsFor(0); - var req = call[0]; - var ctx = call[1]; + const call = bridgeCtrl.onEvent.calls.argsFor(0); + const req = call[0]; + const ctx = call[1]; expect(req.getData()).toEqual(event); expect(ctx.senders.matrix.getId()).toEqual("@foo:bar"); expect(ctx.rooms.matrix.getId()).toEqual("!flibble:bar"); @@ -483,7 +469,7 @@ describe("Bridge", function() { }); it("should include remote senders in the context if applicable", async() => { - var event = { + const event = { content: { body: "oh noes!", msgtype: "m.text" @@ -501,16 +487,15 @@ describe("Bridge", function() { ); await appService.emit("event", event); expect(bridgeCtrl.onEvent).toHaveBeenCalled(); - var call = bridgeCtrl.onEvent.calls.argsFor(0); - var req = call[0]; - var ctx = call[1]; + const call = bridgeCtrl.onEvent.calls.argsFor(0); + const [req, ctx] = call; expect(req.getData()).toEqual(event); expect(ctx.senders.remote.getId()).toEqual("__alice__"); expect(ctx.senders.remotes.length).toEqual(1); }); it("should include remote targets in the context if applicable", async() => { - var event = { + const event = { content: { membership: "invite" }, @@ -528,9 +513,8 @@ describe("Bridge", function() { ); await appService.emit("event", event); expect(bridgeCtrl.onEvent).toHaveBeenCalled(); - var call = bridgeCtrl.onEvent.calls.argsFor(0); - var req = call[0]; - var ctx = call[1]; + const call = bridgeCtrl.onEvent.calls.argsFor(0); + const [req, ctx] = call; expect(req.getData()).toEqual(event); expect(ctx.targets.remote.getId()).toEqual("__bob__"); expect(ctx.targets.remotes.length).toEqual(1); @@ -538,7 +522,7 @@ describe("Bridge", function() { it("should include remote rooms in the context if applicable", function(done) { - var event = { + const event = { content: { membership: "invite" }, @@ -558,9 +542,8 @@ describe("Bridge", function() { return appService.emit("event", event); }).then(function() { expect(bridgeCtrl.onEvent).toHaveBeenCalled(); - var call = bridgeCtrl.onEvent.calls.argsFor(0); - var req = call[0]; - var ctx = call[1]; + const call = bridgeCtrl.onEvent.calls.argsFor(0); + const [req, ctx] = call; expect(req.getData()).toEqual(event); expect(ctx.rooms.remote.getId()).toEqual("roomy"); expect(ctx.rooms.remotes.length).toEqual(1); @@ -587,16 +570,15 @@ describe("Bridge", function() { userStore: userStore, roomStore: roomStore, controller: bridgeCtrl, - clientFactory: clientFactory, - disableContext: true + disableContext: true, + onIntentCreate: (...args) => intentCreateFn(...args), }); await bridge.run(101, appService); await appService.emit("event", event); expect(bridgeCtrl.onEvent).toHaveBeenCalled(); - var call = bridgeCtrl.onEvent.calls.argsFor(0); - var req = call[0]; - var ctx = call[1]; + const call = bridgeCtrl.onEvent.calls.argsFor(0); + const [req, ctx] = call expect(req.getData()).toEqual(event); expect(ctx).toBeNull(); }); @@ -642,7 +624,7 @@ describe("Bridge", function() { describe("getIntent", function() { // 2h which should be long enough to cull it - var cullTimeMs = 1000 * 60 * 60 * 2; + const cullTimeMs = 1000 * 60 * 60 * 2; beforeEach(async() => { jasmine.clock().install(); @@ -656,46 +638,44 @@ describe("Bridge", function() { it("should return the same intent on multiple invokations within the cull time", function() { - var intent = bridge.getIntent("@foo:bar"); + const intent = bridge.getIntent("@foo:bar"); // sentinel. If the same object is returned, this will be present. intent._test = 42; - var intent2 = bridge.getIntent("@foo:bar"); + const intent2 = bridge.getIntent("@foo:bar"); expect(intent).toEqual(intent2); }); it( "should not return the same intent on multiple invokations outside the cull time", function() { - var intent = bridge.getIntent("@foo:bar"); + const intent = bridge.getIntent("@foo:bar"); // sentinel. If the same object is returned, this will be present. intent._test = 42; jasmine.clock().tick(cullTimeMs); - var intent2 = bridge.getIntent("@foo:bar"); + const intent2 = bridge.getIntent("@foo:bar"); expect(intent).not.toEqual(intent2); }); it("should not cull intents which are accessed again via getIntent", function() { - var intent = bridge.getIntent("@foo:bar"); + const intent = bridge.getIntent("@foo:bar"); // sentinel. If the same object is returned, this will be present. intent._test = 42; // Call getIntent 1000 times evenly up to the cull time. If the cull time is // 2hrs, then this is called once every ~7.2s - for (var i = 0; i < 1000; i ++) { + for (let i = 0; i < 1000; i ++) { jasmine.clock().tick(cullTimeMs/1000); bridge.getIntent("@foo:bar"); } - var intent2 = bridge.getIntent("@foo:bar"); + const intent2 = bridge.getIntent("@foo:bar"); expect(intent).toEqual(intent2); }); - it("should keep the Intent up-to-date with incoming events", function(done) { - var client = mkMockMatrixClient("@foo:bar"); - client.joinRoom.and.returnValue(Promise.resolve({})); // shouldn't be called - clients["@foo:bar"] = client; + it("should keep the Intent up-to-date with incoming events", async() => { + const intent = bridge.getIntent("@foo:bar"); + intent.botSdkIntent.underlyingClient.joinRoom.and.returnValue(Promise.resolve({})); // shouldn't be called - var intent = bridge.getIntent("@foo:bar"); - var joinEvent = { + const joinEvent = { content: { membership: "join" }, @@ -705,24 +685,20 @@ describe("Bridge", function() { type: "m.room.member" }; appService.emit("event", joinEvent); - intent.join("!flibble:bar").then(() => { - expect(client.joinRoom).not.toHaveBeenCalled(); - done(); - }); + await intent.join("!flibble:bar"); + expect(intent.botSdkIntent.underlyingClient.joinRoom).not.toHaveBeenCalled(); }); - it("should keep culled Intents up-to-date with incoming events", function(done) { + it("should keep culled Intents up-to-date with incoming events", function() { // We tell the bridge that @foo:bar is joined to the room. // Therefore, we expect that intent.join() should NOT call the SDK's join // method. This should still be the case even if the Intent object is culled // and we try to join using a new intent, in addition to if we use the old // stale Intent. - var client = mkMockMatrixClient("@foo:bar"); - client.joinRoom.and.returnValue(Promise.resolve({})); // shouldn't be called - clients["@foo:bar"] = client; + const intent = bridge.getIntent("@foo:bar"); + intent.botSdkIntent.underlyingClient.joinRoom.and.returnValue(Promise.resolve({})); // shouldn't be called - var intent = bridge.getIntent("@foo:bar"); - var joinEvent = { + const joinEvent = { content: { membership: "join" }, @@ -735,21 +711,20 @@ describe("Bridge", function() { // wait the cull time then attempt the join, it shouldn't try to join. jasmine.clock().tick(cullTimeMs); - intent.join("!flibble:bar").then(function() { - expect(client.joinRoom).not.toHaveBeenCalled(); + return intent.join("!flibble:bar").then(function() { + expect(intent.botSdkIntent.underlyingClient.joinRoom).not.toHaveBeenCalled(); // wait the cull time again and use a new intent, still shouldn't join. jasmine.clock().tick(cullTimeMs); return bridge.getIntent("@foo:bar").join("!flibble:bar"); }).then(() => { - expect(client.joinRoom).not.toHaveBeenCalled(); - done(); + expect(intent.botSdkIntent.underlyingClient.joinRoom).not.toHaveBeenCalled(); }); }); it("should scope Intents to a request if provided", function() { - var intent = bridge.getIntent("@foo:bar"); + const intent = bridge.getIntent("@foo:bar"); intent._test = 42; // sentinel - var intent2 = bridge.getIntent("@foo:bar", { + const intent2 = bridge.getIntent("@foo:bar", { getId: function() { return "request id here"; } }); expect(intent2).toBeDefined(); @@ -759,105 +734,107 @@ describe("Bridge", function() { it("should return an escaped userId", function() { const intent = bridge.getIntent("@foo£$&!£:bar"); - expect(intent.client.uid).toEqual("@foo=a3=24=26=21=a3:bar"); + expect(intent.userId).toEqual("@foo=a3=24=26=21=a3:bar"); }); it("should not return an escaped userId if disabled", function() { bridge.opts.escapeUserIds = false; const intent = bridge.getIntent("@foo£$&!£:bar"); - expect(intent.client.uid).toEqual("@foo£$&!£:bar"); + expect(intent.userId).toEqual("@foo£$&!£:bar"); }); }); describe("provisionUser", function() { beforeEach(() => { + userIsRegistered = false; return bridge.initalise(); }); + afterAll(() => { + userIsRegistered = true; + }) + it("should provision a user with the specified user ID", function() { - var mxUser = new MatrixUser("@foo:bar"); - var provisionedUser = {}; - //botClient.register.and.returnValue(Promise.resolve({})); + const mxUser = new MatrixUser("@foo:example.com"); + const provisionedUser = {}; + const intent = bridge.getIntent(mxUser.getId()); + intent.botSdkIntent.ensureRegistered.and.returnValue(Promise.resolve({})); return bridge.provisionUser(mxUser, provisionedUser).then(function() { - expect(botClient.register).toHaveBeenCalledWith(mxUser.localpart); + expect(intent.botSdkIntent.ensureRegistered).toHaveBeenCalled(); // should also be persisted in storage - return bridge.getUserStore().getMatrixUser("@foo:bar"); + return bridge.getUserStore().getMatrixUser("@foo:example.com"); }).then((usr) => { expect(usr).toBeDefined(); - expect(usr.getId()).toEqual("@foo:bar"); + expect(usr.getId()).toEqual("@foo:example.com"); }); }); it("should set the display name if one was provided", function() { - var mxUser = new MatrixUser("@foo:bar"); - var provisionedUser = { + const mxUser = new MatrixUser("@foo:example.com"); + const provisionedUser = { name: "Foo Bar" }; - var botClient = bridge.botIntent.botSdkIntent; - botClient.register.and.returnValue(Promise.resolve({})); - var client = mkMockMatrixClient("@foo:bar"); - client.setDisplayName.and.returnValue(Promise.resolve({})); - clients["@foo:bar"] = client; + const intent = bridge.getIntent(mxUser.getId()); + const botClient = intent.botSdkIntent; + botClient.ensureRegistered.and.returnValue(Promise.resolve({})); + botClient.underlyingClient.setDisplayName.and.returnValue(Promise.resolve({})); return bridge.provisionUser(mxUser, provisionedUser).then(() => { - expect(botClient.register).toHaveBeenCalledWith(mxUser.localpart); - expect(client.setDisplayName).toHaveBeenCalledWith("Foo Bar"); + expect(botClient.ensureRegistered).toHaveBeenCalled(); + expect(botClient.underlyingClient.setDisplayName).toHaveBeenCalledWith("Foo Bar"); }); }); it("should set the avatar URL if one was provided", function() { - var mxUser = new MatrixUser("@foo:bar"); - var provisionedUser = { - url: "http://avatar.jpg" + const mxUser = new MatrixUser("@foo:example.com"); + const provisionedUser = { + url: "mxc://server/avatar.jpg" }; - var botClient = bridge.botIntent.botSdkIntent; - botClient.register.and.returnValue(Promise.resolve({})); - var client = mkMockMatrixClient("@foo:bar"); - client.setAvatarUrl.and.returnValue(Promise.resolve({})); - clients["@foo:bar"] = client; + const intent = bridge.getIntent(mxUser.getId()); + const botClient = intent.botSdkIntent; + botClient.ensureRegistered.and.returnValue(Promise.resolve({})); + botClient.underlyingClient.setAvatarUrl.and.returnValue(Promise.resolve({})); return bridge.provisionUser(mxUser, provisionedUser).then(() => { - expect(botClient.register).toHaveBeenCalledWith(mxUser.localpart); - expect(client.setAvatarUrl).toHaveBeenCalledWith("http://avatar.jpg"); + expect(botClient.ensureRegistered).toHaveBeenCalled(); + expect(botClient.underlyingClient.setAvatarUrl).toHaveBeenCalledWith("mxc://server/avatar.jpg"); }); }); it("should link the user with a remote user if one was provided", - function(done) { - var mxUser = new MatrixUser("@foo:bar"); - var provisionedUser = { + function() { + const mxUser = new MatrixUser("@foo:example.com"); + const provisionedUser = { remote: new RemoteUser("__remote__") }; - var botClient = bridge.botIntent.botSdkIntent; - botClient.register.and.returnValue(Promise.resolve({})); - var client = mkMockMatrixClient("@foo:bar"); - clients["@foo:bar"] = client; - bridge.provisionUser(mxUser, provisionedUser).then(function() { - expect(botClient.register).toHaveBeenCalledWith(mxUser.localpart); - return bridge.getUserStore().getRemoteUsersFromMatrixId("@foo:bar"); + const intent = bridge.getIntent(mxUser.getId()); + const botClient = intent.botSdkIntent; + botClient.ensureRegistered.and.returnValue(Promise.resolve({})); + return bridge.provisionUser(mxUser, provisionedUser).then(function() { + expect(botClient.ensureRegistered).toHaveBeenCalled(); + return bridge.getUserStore().getRemoteUsersFromMatrixId("@foo:example.com"); }).then(function(users) { expect(users.length).toEqual(1); if (users.length > 0) { expect(users[0].getId()).toEqual("__remote__"); } - done(); }); }); - it("should fail if the HTTP registration fails", function(done) { - var mxUser = new MatrixUser("@foo:bar"); - var provisionedUser = {}; - var botClient = bridge.botIntent.botSdkIntent; - const err = { errcode: "M_FORBIDDEN" }; - const errorPromise = Promise.reject(err) - botClient.register.and.returnValue(errorPromise); - bridge.provisionUser(mxUser, provisionedUser).catch(function(ex) { - expect(ex).toBe(err); - expect(botClient.register).toHaveBeenCalledWith(mxUser.localpart); - done(); - }); + it("should fail if the HTTP registration fails", function() { + const provisionedUser = {}; + const mxUser = new MatrixUser("@foo:example.com"); + const intent = bridge.getIntent(mxUser.getId()); + const botClient = intent.botSdkIntent; + const err = { errcode: "M_FORBIDDEN" }; + const errorPromise = Promise.reject({ errcode: "M_FORBIDDEN" }) // This complains otherwise. errorPromise.catch((ex) => {}); + botClient.ensureRegistered.and.returnValue(errorPromise); + return bridge.provisionUser(mxUser, provisionedUser).catch(function(ex) { + expect(ex).toEqual(err); + expect(botClient.ensureRegistered).toHaveBeenCalled(); + }); }); }); @@ -924,27 +901,4 @@ describe("Bridge", function() { }); }); }); -}); - -function mkMockMatrixClient(uid) { - const client = jasmine.createSpyObj( - "MatrixClient", [ - "register", "joinRoom", "credentials", "createRoom", "setDisplayName", - "setAvatarUrl", "fetchRoomEvent", "getJoinedRoomMembers", "_http" - ] - ); - // Shim requests to authedRequestWithPrefix to register() if it is - // directed at /register - // client._http.authedRequest.and.callFake(((method, endpoint, qs, body, timeout, raw, contentType, noEncoding) => { - // console.log(method, endpoint, qs, body, timeout, raw, contentType, noEncoding); - // }); - client._http.authedRequest = jasmine.createSpy("authedRequest"); - client._http.authedRequest.and.callFake(function(a, method, path, d, data) { - if (method === "POST" && path === "/register") { - return client.register(data.user); - } - return undefined; - }); - client.credentials.userId = uid; - return client; -} +}); \ No newline at end of file diff --git a/src/bridge.ts b/src/bridge.ts index 915d5405..0de4e119 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -199,6 +199,10 @@ export interface BridgeOpts { */ clients?: IntentOpts; }; + /** + * The factory function used to create intents. + */ + onIntentCreate?: (userId: string) => Intent, /** * Options for the `onEvent` queue. When the bridge * receives an incoming transaction, it needs to asyncly query the data store for @@ -345,6 +349,10 @@ interface VettedBridgeOpts { */ clients?: IntentOpts; }; + /** + * The factory function used to create intents. + */ + onIntentCreate: (userId: string, opts: IntentOpts) => Intent, /** * Options for the `onEvent` queue. When the bridge * receives an incoming transaction, it needs to asyncly query the data store for @@ -435,11 +443,14 @@ export class Bridge { public readonly opts: VettedBridgeOpts; - public get appService() { + public get appService(): AppService { + if (!this.appservice) { + throw Error('appservice not defined yet'); + } return this.appservice; } - public get botUserId() { + public get botUserId(): string { if (!this.registration) { throw Error('Registration not defined yet'); } @@ -475,6 +486,7 @@ export class Bridge { userStore: opts.userStore || "user-store.db", roomStore: opts.roomStore || "room-store.db", intentOptions: opts.intentOptions || {}, + onIntentCreate: opts.onIntentCreate ?? this.onIntentCreate.bind(this), queue: { type: opts.queue?.type || "single", perRequest: opts.queue?.perRequest ?? false, @@ -526,7 +538,7 @@ export class Bridge { /** * Load the user and room databases. Access them via getUserStore() and getRoomStore(). */ - public async loadDatabases() { + public async loadDatabases(): Promise { if (this.opts.disableStores) { return; } @@ -566,7 +578,7 @@ export class Bridge { * * **This must be called before `listen()`** */ - public async initalise() { + public async initalise(): Promise { if (typeof this.opts.registration === "string") { const regObj = yaml.load(await fs.readFile(this.opts.registration, 'utf8')); if (typeof regObj !== "object") { @@ -656,6 +668,7 @@ export class Bridge { ) ); } + const botIntentOpts: IntentOpts = { registered: true, backingStore: this.intentBackingStore, @@ -672,8 +685,7 @@ export class Bridge { }, ...this.opts.intentOptions?.bot, // copy across opts, if defined }; - - this.botIntent = new Intent(this.botSdkAS.botIntent, this.botSdkAS.botClient, botIntentOpts); + this.botIntent = this.opts.onIntentCreate(this.botUserId, botIntentOpts); this.setupIntentCulling(); @@ -975,7 +987,7 @@ export class Bridge { checkToken?: boolean, path: string, handler: (req: ExRequest, respose: ExResponse, next: NextFunction) => void, - }) { + }): void { if (!this.appservice) { throw Error('Cannot call addAppServicePath before calling .run()'); } @@ -997,37 +1009,38 @@ export class Bridge { } /** - * Retrieve the connected room store instance. + * Retrieve the connected room store instance, if one was configured. */ - public getRoomStore() { + public getRoomStore(): RoomBridgeStore|undefined { return this.roomStore; } /** - * Retrieve the connected user store instance. + * Retrieve the connected user store instance, if one was configured. */ - public getUserStore() { + public getUserStore(): UserBridgeStore|undefined { return this.userStore; } /** * Retrieve the connected event store instance, if one was configured. */ - public getEventStore() { + public getEventStore(): EventBridgeStore|undefined { return this.eventStore; } /** * Retrieve the request factory used to create incoming requests. */ - public getRequestFactory() { + public getRequestFactory(): RequestFactory { return this.requestFactory; } /** * Retrieve the matrix client factory used when sending matrix requests. + * @deprecated The client factory is deprecated. */ - public getClientFactory() { + public getClientFactory(): ClientFactory { if (!this.clientFactory) { throw Error('Bridge is not ready'); } @@ -1037,7 +1050,7 @@ export class Bridge { /** * Get the AS bot instance. */ - public getBot() { + public getBot(): AppServiceBot { if (!this.appServiceBot) { throw Error('Bridge is not ready'); } @@ -1072,7 +1085,7 @@ export class Bridge { * instance to. Useful for logging contextual request IDs. * @return The intent instance */ - public getIntent(userId?: string, request?: Request) { + public getIntent(userId?: string, request?: Request): Intent { if (!this.appServiceBot || !this.botSdkAS) { throw Error('Cannot call getIntent before calling .initalise()'); } @@ -1132,11 +1145,8 @@ export class Bridge { }, }; } - const intent = new Intent( - this.botSdkAS.getIntentForUserId(userId), - this.botSdkAS.botClient, - clientIntentOpts, - ); + + const intent = this.opts.onIntentCreate(userId, clientIntentOpts); this.intents.set(key, { intent, lastAccessed: Date.now() }); return intent; @@ -1150,7 +1160,7 @@ export class Bridge { * instance to. Useful for logging contextual request IDs. * @return The intent instance */ - public getIntentFromLocalpart(localpart: string, request?: Request) { + public getIntentFromLocalpart(localpart: string, request?: Request): Intent { return this.getIntent( "@" + localpart + ":" + this.opts.domain, request, ); @@ -1266,18 +1276,18 @@ export class Bridge { * Find a member for a given room. This method will fetch the joined members * from the homeserver if the cache doesn't have it stored. * @param preferBot Should we prefer the bot user over a ghost user - * @returns {Promise} The userID of the member. + * @returns The userID of the member. */ - public async getAnyASMemberInRoom(roomId: string, preferBot = true) { + public async getAnyASMemberInRoom(roomId: string, preferBot = true): Promise { if (!this.registration) { throw Error('Registration must be defined before you can call this'); } let members = this.membershipCache.getMembersForRoom(roomId, "join"); if (!members) { - if (!this.appServiceBot) { + if (!this.botIntent) { throw Error('AS Bot not defined yet'); } - members = Object.keys(await this.appServiceBot.getJoinedMembers(roomId)); + members = await this.botIntent.botSdkIntent.underlyingClient.getJoinedRoomMembers(roomId); } if (preferBot && members?.includes(this.botUserId)) { return this.botUserId; @@ -1286,7 +1296,8 @@ export class Bridge { return members.find((u) => reg.isUserMatch(u, false)) || null; } - private async validateEditEvent(event: WeakEvent, parentEventId: string, allowEventOnLookupFail: boolean) { + private async validateEditEvent( + event: WeakEvent, parentEventId: string, allowEventOnLookupFail: boolean): Promise { try { const roomMember = await this.getAnyASMemberInRoom(event.room_id); if (!roomMember) { @@ -1323,7 +1334,7 @@ export class Bridge { } if (this.selfPingDeferred?.roomId === event.room_id && event.sender === this.botUserId) { this.selfPingDeferred.defer.resolve(); - log.debug("Got self ping") + log.debug("Got self ping"); return null; } const isCanonicalState = event.state_key === ""; @@ -1496,6 +1507,7 @@ export class Bridge { if (content && content.avatar_url) { profile.avatar_url = content.avatar_url; } + console.log("CACHED membership entry!", event.room_id, event.state_key, content.membership); this.membershipCache.setMemberEntry( event.room_id, event.state_key, @@ -1565,7 +1577,7 @@ export class Bridge { * } * }) */ - public registerBridgeGauges(counterFunc: () => Promise|BridgeGaugesCounts) { + public registerBridgeGauges(counterFunc: () => Promise|BridgeGaugesCounts): void { this.getPrometheusMetrics().registerBridgeGauges(async () => { const counts = await counterFunc(); if (counts.matrixGhosts !== undefined) { @@ -1581,7 +1593,7 @@ export class Bridge { * and the `Authorization` header are checked. * @returns {Boolean} True if authenticated, False if not. */ - public requestCheckToken(req: ExRequest) { + public requestCheckToken(req: ExRequest): boolean { if (!this.registration) { // Bridge isn't ready yet return false; @@ -1599,7 +1611,7 @@ export class Bridge { * Close the appservice HTTP listener, and clear all timeouts. * @returns Resolves when the appservice HTTP listener has stopped */ - public async close() { + public async close(): Promise { if (this.intentLastAccessedTimeout) { clearTimeout(this.intentLastAccessedTimeout); this.intentLastAccessedTimeout = null; @@ -1614,7 +1626,7 @@ export class Bridge { } - public async checkHomeserverSupport() { + public async checkHomeserverSupport(): Promise { if (!this.botSdkAS) { throw Error("botSdkAS isn't ready yet"); } @@ -1637,7 +1649,7 @@ export class Bridge { * @throws This will throw if another ping attempt is made, or if the request times out. * @returns The delay in milliseconds */ - public async pingAppserviceRoute(roomId: string, timeoutMs = BRIDGE_PING_TIMEOUT_MS) { + public async pingAppserviceRoute(roomId: string, timeoutMs = BRIDGE_PING_TIMEOUT_MS): Promise { if (!this.botIntent) { throw Error("botIntent isn't ready yet"); } @@ -1660,6 +1672,15 @@ export class Bridge { return Date.now() - sentTs; } + private onIntentCreate(userId: string, intentOpts: IntentOpts) { + if (!this.botSdkAS) { + throw Error('botSdkAS must be defined before onIntentCreate can be called'); + } + const isBot = this.botUserId === userId; + const botIntent = isBot ? this.botSdkAS.botIntent : this.botSdkAS.getIntentForUserId(userId); + return new Intent(botIntent, this.botSdkAS.botClient, intentOpts); + } + } function loadDatabase(path: string, Cls: new (db: Datastore) => T) { diff --git a/src/components/app-service-bot.ts b/src/components/app-service-bot.ts index 0c3552b7..3fbf5e3d 100644 --- a/src/components/app-service-bot.ts +++ b/src/components/app-service-bot.ts @@ -17,7 +17,7 @@ limitations under the License. import { AppServiceRegistration } from "matrix-appservice"; import { MembershipCache, UserProfile } from "./membership-cache"; import { StateLookupEvent } from ".."; -import { MatrixClient } from "matrix-bot-sdk"; +import { MatrixClient, MatrixProfileInfo } from "matrix-bot-sdk"; /** * Construct an AS bot user which has various helper methods. @@ -67,10 +67,12 @@ export class AppServiceBot { * @param roomId The room to get a list of joined user IDs in. * @return Resolves to a map of user ID => display_name avatar_url */ - public async getJoinedMembers(roomId: string) { + public async getJoinedMembers(roomId: string): Promise<{[userId: string]: MatrixProfileInfo}> { + // Until https://github.com/turt2live/matrix-bot-sdk/pull/138 is merged. // eslint-disable-next-line camelcase - const res = await this.client.getJoinedRoomMembersWithProfiles(roomId); - for (const [member, p] of Object.entries(res.joined)) { + const res: Record = + await this.client.getJoinedRoomMembersWithProfiles(roomId); + for (const [member, p] of Object.entries(res)) { if (this.isRemoteUser(member)) { const profile: UserProfile = {}; if (p.display_name) { @@ -82,7 +84,7 @@ export class AppServiceBot { this.memberCache.setMemberEntry(roomId, member, "join", profile); } } - return res.joined; + return res; } public async getRoomInfo(roomId: string, joinedRoom: {state?: { events: StateLookupEvent[]}} = {}) { diff --git a/src/components/intent.ts b/src/components/intent.ts index a43a36c2..a499deab 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -974,8 +974,7 @@ export class Intent { opts.viaServers = viaServers; } // Resolve the alias - const roomId = await this.botClient.resolveRoom(roomIdOrAlias); - + const roomId = await this.resolveRoom(roomIdOrAlias); if (!ignoreCache && this.opts.backingStore.getMembership(roomId, this.userId) === "join") { return roomId; } From 77f02a39db5ba351d9a996eb380ebbb023bbbe6d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 9 Aug 2021 16:49:02 +0100 Subject: [PATCH 11/21] Update tests --- spec/unit/intent.spec.js | 410 +++++++++++++++++++-------------------- 1 file changed, 199 insertions(+), 211 deletions(-) diff --git a/spec/unit/intent.spec.js b/spec/unit/intent.spec.js index b010d3a4..4ce8531c 100644 --- a/spec/unit/intent.spec.js +++ b/spec/unit/intent.spec.js @@ -1,7 +1,7 @@ const { Intent } = require("../.."); describe("Intent", function() { - let intent, botIntent, client, botClient; + let intent, botIntent, client, botClient, underlyingClient; const userId = "@alice:bar"; const botUserId = "@bot:user"; const roomId = "!foo:bar"; @@ -9,22 +9,15 @@ describe("Intent", function() { registered: true }; - beforeEach( - /** @this */ - function() { - const clientFields = [ - "credentials", "joinRoom", "resolveRoom", "inviteUser", "leave", "ban", "unban", - "kick", "getRoomStateEvent", "setUserPowerLevel", "sendTyping", "sendEvent", - "sendStateEvent", "setDisplayName", "setAvatarUrl", "getUserId", - ]; - client = jasmine.createSpyObj("botSdkIntentClient", clientFields); + beforeEach(function() { + const clientFields = ["joinRoom", "resolveRoom", "inviteUser", "sendStateEvent", "setUserPowerLevel", "getUserId", "sendEvent"]; + underlyingClient = jasmine.createSpyObj("underlyingClient", clientFields); botIntent = { userId, - underlyingClient: client, + underlyingClient, }; - client.credentials.userId = userId; botClient = jasmine.createSpyObj("botClient", clientFields); - botClient.resolveRoom.and.callFake(async () => roomId); + underlyingClient.resolveRoom.and.callFake(async () => roomId); botClient.getUserId.and.callFake(async () => botUserId); intent = new Intent(botIntent, botClient, alreadyRegistered); }); @@ -33,9 +26,9 @@ describe("Intent", function() { it("should /join/$ROOMID if it doesn't know it is already joined", function() { - client.joinRoom.and.callFake(async () => roomId); + underlyingClient.joinRoom.and.callFake(async () => roomId); return intent.join(roomId).then(function(resultRoomId) { - expect(client.joinRoom).toHaveBeenCalledWith( + expect(underlyingClient.joinRoom).toHaveBeenCalledWith( roomId, undefined, ); expect(resultRoomId).toBe(roomId); @@ -44,11 +37,11 @@ describe("Intent", function() { it("should /join/$ROOMID if it doesn't know it is already joined with via parameters", function() { const via = ["foo.org", "bar.org"]; - client.joinRoom.and.callFake(async () =>{ + underlyingClient.joinRoom.and.callFake(async () =>{ return roomId; }); return intent.join(roomId, via).then(function(resultRoomId) { - expect(client.joinRoom).toHaveBeenCalledWith( + expect(underlyingClient.joinRoom).toHaveBeenCalledWith( roomId, via, ); expect(resultRoomId).toBe(roomId); @@ -67,25 +60,25 @@ describe("Intent", function() { }); return intent.join(roomId).then(function(resultRoomId) { expect(resultRoomId).toBe(roomId); - expect(client.joinRoom).not.toHaveBeenCalled(); + expect(underlyingClient.joinRoom).not.toHaveBeenCalled(); }); }); it("should fail if the join returned an error other than forbidden", function() { - client.joinRoom.and.callFake(() => Promise.reject({ + underlyingClient.joinRoom.and.callFake(() => Promise.reject({ errcode: "M_YOU_ARE_A_FISH", error: "you're a fish" })); return intent.join(roomId).catch(function() { - expect(client.joinRoom).toHaveBeenCalled(); + expect(underlyingClient.joinRoom).toHaveBeenCalled(); }); }); describe("client join failed", function() { it("should make the bot invite then the client join", function() { - client.joinRoom.and.callFake(function() { + underlyingClient.joinRoom.and.callFake(function() { if (botClient.inviteUser.calls.count() === 0) { return Promise.reject({ errcode: "M_FORBIDDEN", @@ -97,7 +90,7 @@ describe("Intent", function() { botClient.inviteUser.and.callFake(() => Promise.resolve({})); return intent.join(roomId).then(function(resultRoomId) { - expect(client.joinRoom).toHaveBeenCalledWith( + expect(underlyingClient.joinRoom).toHaveBeenCalledWith( roomId, undefined ); expect(botClient.inviteUser).toHaveBeenCalledWith(userId, roomId); @@ -108,7 +101,7 @@ describe("Intent", function() { describe("bot invite failed", function() { it("should make the bot join then invite then the client join", function() { - client.joinRoom.and.callFake(function() { + underlyingClient.joinRoom.and.callFake(function() { if (botClient.inviteUser.calls.count() === 0) { return Promise.reject({ errcode: "M_FORBIDDEN", @@ -129,7 +122,7 @@ describe("Intent", function() { botClient.joinRoom.and.callFake(() => Promise.resolve({roomId: roomId})); return intent.join(roomId).then(function(resultRoomId) { - expect(client.joinRoom).toHaveBeenCalledWith( + expect(underlyingClient.joinRoom).toHaveBeenCalledWith( roomId, undefined ); expect(botClient.inviteUser).toHaveBeenCalledWith(userId, roomId); @@ -142,7 +135,7 @@ describe("Intent", function() { }); it("should give up if the bot cannot join the room", function() { - client.joinRoom.and.callFake(() => Promise.reject({ + underlyingClient.joinRoom.and.callFake(() => Promise.reject({ errcode: "M_FORBIDDEN", error: "Join first" })); @@ -156,7 +149,7 @@ describe("Intent", function() { })); return intent.join(roomId).catch(function() { - expect(client.joinRoom).toHaveBeenCalledWith( + expect(underlyingClient.joinRoom).toHaveBeenCalledWith( roomId, undefined ); expect(botClient.inviteUser).toHaveBeenCalledWith(userId, roomId); @@ -213,11 +206,11 @@ describe("Intent", function() { it("should directly send the event if it thinks power levels are ok", function() { - client.sendStateEvent.and.returnValue(Promise.resolve("$foo:bar")); + underlyingClient.sendStateEvent.and.returnValue(Promise.resolve("$foo:bar")); intent.onEvent(validPowerLevels); return intent.setRoomTopic(roomId, "Hello world").then(function() { - expect(client.sendStateEvent).toHaveBeenCalledWith( + expect(underlyingClient.sendStateEvent).toHaveBeenCalledWith( roomId, "m.room.topic", "", {topic: "Hello world"} ); }) @@ -225,11 +218,10 @@ describe("Intent", function() { it("should modify power levels before sending if client is too low", async function() { - client.sendStateEvent.and.callFake(function() { - if (client.sendStateEvent.calls.count() > 1) { + underlyingClient.sendStateEvent.and.callFake(function() { + if (underlyingClient.sendStateEvent.calls.count() > 1) { return Promise.resolve({}); } - console.log("ARGH", client.sendStateEvent.calls.count()); return Promise.reject({ errcode: "M_FORBIDDEN", error: "Not enough powaaaaaa" @@ -241,194 +233,190 @@ describe("Intent", function() { intent.onEvent(invalidPowerLevels); await intent.setRoomTopic(roomId, "Hello world"); - console.log("ABC"); - expect(client.sendStateEvent).toHaveBeenCalledWith( + expect(underlyingClient.sendStateEvent).toHaveBeenCalledWith( roomId, "m.room.topic", "", {topic: "Hello world"} ); - console.log("ABC"); expect(botClient.setUserPowerLevel).toHaveBeenCalledWith( userId, roomId, 50 ); }); - // it("should fail if the bot cannot modify power levels and the client is too low", - // function() { - // // bot has NO power - // intent.onEvent(invalidPowerLevels); + it("should fail if the bot cannot modify power levels and the client is too low", + function() { + // bot has NO power + intent.onEvent(invalidPowerLevels); - // return intent.setRoomTopic(roomId, "Hello world").catch(function() { - // expect(client.sendStateEvent).not.toHaveBeenCalled(); - // expect(botClient.setUserPowerLevel).not.toHaveBeenCalled(); - // }) - // }); + return intent.setRoomTopic(roomId, "Hello world").catch(function() { + expect(underlyingClient.sendStateEvent).not.toHaveBeenCalled(); + expect(botClient.setUserPowerLevel).not.toHaveBeenCalled(); + }) + }); }); - // describe("sending message events", function() { - // const content = { - // body: "hello world", - // msgtype: "m.text", - // }; - - // beforeEach(function() { - // intent = new Intent(client, botClient, { - // ...alreadyRegistered, - // dontCheckPowerLevel: true, - // }); - // // not interested in joins, so no-op them. - // intent.onEvent({ - // event_id: "test", - // type: "m.room.member", - // state_key: userId, - // room_id: roomId, - // content: { - // membership: "join" - // } - // }); - // }); - - // it("should immediately try to send the event if joined/have pl", function() { - // client.sendEvent.and.returnValue(Promise.resolve({ - // event_id: "$abra:kadabra" - // })); - // return intent.sendMessage(roomId, content).then(function() { - // expect(client.sendEvent).toHaveBeenCalledWith( - // roomId, "m.room.message", content - // ); - // expect(client.joinRoom).not.toHaveBeenCalled(); - // }); - // }); - - // it("should fail if get an error that isn't M_FORBIDDEN", function() { - // client.sendEvent.and.callFake(() => Promise.reject({ - // error: "Oh no", - // errcode: "M_UNKNOWN" - // })); - // return intent.sendMessage(roomId, content).catch(function() { - // expect(client.sendEvent).toHaveBeenCalledWith( - // roomId, "m.room.message", content - // ); - // expect(client.joinRoom).not.toHaveBeenCalled(); - // }); - // }); - - // it("should try to join the room on M_FORBIDDEN then resend", function() { - // let isJoined = false; - // client.sendEvent.and.callFake(function() { - // if (isJoined) { - // return Promise.resolve({ - // event_id: "$12345:6789" - // }); - // } - // return Promise.reject({ - // error: "You are not joined", - // errcode: "M_FORBIDDEN" - // }); - // }); - // client.joinRoom.and.callFake(function(joinRoomId) { - // isJoined = true; - // return Promise.resolve(joinRoomId); - // }); - // return intent.sendMessage(roomId, content).then(function(eventId) { - // expect(client.sendEvent).toHaveBeenCalledWith( - // roomId, "m.room.message", content - // ); - // expect(client.joinRoom).toHaveBeenCalledWith(roomId, undefined); - // expect(eventId).toEqual({event_id: "$12345:6789"}); - // }); - // }); - - // it("should fail if the join on M_FORBIDDEN fails", function() { - // client.sendEvent.and.callFake(function() { - // return Promise.reject({ - // error: "You are not joined", - // errcode: "M_FORBIDDEN" - // }); - // }); - // client.joinRoom.and.callFake(() => Promise.reject({ - // error: "Never!", - // errcode: "M_YOU_ARE_A_FISH" - // })); - // return intent.sendMessage(roomId, content).catch(function() { - // expect(client.sendEvent).toHaveBeenCalledWith( - // roomId, "m.room.message", content - // ); - // expect(client.joinRoom).toHaveBeenCalledWith(roomId, undefined); - // }); - // }); - - // it("should fail if the resend after M_FORBIDDEN fails", function() { - // let isJoined = false; - // client.sendEvent.and.callFake(function() { - // if (isJoined) { - // return Promise.reject({ - // error: "Internal Server Error", - // errcode: "M_WHOOPSIE", - // }); - // } - // return Promise.reject({ - // error: "You are not joined", - // errcode: "M_FORBIDDEN", - // }); - // }); - // client.joinRoom.and.callFake(function(joinRoomId) { - // isJoined = true; - // return Promise.resolve(joinRoomId); - // }); - // return intent.sendMessage(roomId, content).catch(function() { - // expect(client.sendEvent).toHaveBeenCalledWith( - // roomId, "m.room.message", content - // ); - // expect(client.joinRoom).toHaveBeenCalledWith(roomId, undefined); - // }); - // }); - // }); - - // describe("signaling bridge error", function() { - // const reason = "m.event_not_handled" - // let affectedUsers, eventId, bridge; - - // beforeEach(function() { - // intent = new Intent(client, botClient, { - // ...alreadyRegistered, - // dontCheckPowerLevel: true, - // }); - // // not interested in joins, so no-op them. - // intent.onEvent({ - // event_id: "test", - // type: "m.room.member", - // state_key: userId, - // room_id: roomId, - // content: { - // membership: "join" - // } - // }); - // eventId = "$random:event.id"; - // bridge = "International Pidgeon Post"; - // affectedUsers = ["@_pidgeonpost_.*:home.server"]; - // }); - - // it("should send an event", function() { - // client.sendEvent.and.returnValue(Promise.resolve({ - // event_id: "$abra:kadabra" - // })); - // return intent - // .unstableSignalBridgeError(roomId, eventId, bridge, reason, affectedUsers) - // .then(() => { - // expect(client.sendEvent).toHaveBeenCalledWith( - // roomId, - // "de.nasnotfound.bridge_error", - // { - // "network_name": bridge, - // "reason": reason, - // "affected_users": affectedUsers, - // "m.relates_to": { - // "rel_type": "m.reference", - // "event_id": eventId - // } - // } - // ); - // expect(client.joinRoom).not.toHaveBeenCalled(); - // }); - // }); - // }); + describe("sending message events", function() { + const content = { + body: "hello world", + msgtype: "m.text", + }; + + beforeEach(function() { + intent = new Intent(botIntent, botClient, { + ...alreadyRegistered, + dontCheckPowerLevel: true, + }); + // not interested in joins, so no-op them. + intent.onEvent({ + event_id: "test", + type: "m.room.member", + state_key: userId, + room_id: roomId, + content: { + membership: "join" + } + }); + }); + + it("should immediately try to send the event if joined/have pl", function() { + underlyingClient.sendEvent.and.returnValue(Promise.resolve({ + event_id: "$abra:kadabra" + })); + return intent.sendMessage(roomId, content).then(function() { + expect(underlyingClient.sendEvent).toHaveBeenCalledWith( + roomId, "m.room.message", content + ); + expect(underlyingClient.joinRoom).not.toHaveBeenCalled(); + }); + }); + + it("should fail if get an error that isn't M_FORBIDDEN", function() { + underlyingClient.sendEvent.and.callFake(() => Promise.reject({ + error: "Oh no", + errcode: "M_UNKNOWN" + })); + return intent.sendMessage(roomId, content).catch(function() { + expect(underlyingClient.sendEvent).toHaveBeenCalledWith( + roomId, "m.room.message", content + ); + expect(underlyingClient.joinRoom).not.toHaveBeenCalled(); + }); + }); + + it("should try to join the room on M_FORBIDDEN then resend", function() { + let isJoined = false; + underlyingClient.sendEvent.and.callFake(function() { + if (isJoined) { + return Promise.resolve("$12345:6789"); + } + return Promise.reject({ + error: "You are not joined", + errcode: "M_FORBIDDEN" + }); + }); + underlyingClient.joinRoom.and.callFake(function(joinRoomId) { + isJoined = true; + return Promise.resolve(joinRoomId); + }); + return intent.sendMessage(roomId, content).then(function(eventId) { + expect(underlyingClient.sendEvent).toHaveBeenCalledWith( + roomId, "m.room.message", content + ); + expect(underlyingClient.joinRoom).toHaveBeenCalledWith(roomId, undefined); + expect(eventId).toEqual({event_id: "$12345:6789"}); + }); + }); + + it("should fail if the join on M_FORBIDDEN fails", function() { + underlyingClient.sendEvent.and.callFake(function() { + return Promise.reject({ + error: "You are not joined", + errcode: "M_FORBIDDEN" + }); + }); + underlyingClient.joinRoom.and.callFake(() => Promise.reject({ + error: "Never!", + errcode: "M_YOU_ARE_A_FISH" + })); + return intent.sendMessage(roomId, content).catch(function() { + expect(underlyingClient.sendEvent).toHaveBeenCalledWith( + roomId, "m.room.message", content + ); + expect(underlyingClient.joinRoom).toHaveBeenCalledWith(roomId, undefined); + }); + }); + + it("should fail if the resend after M_FORBIDDEN fails", function() { + let isJoined = false; + underlyingClient.sendEvent.and.callFake(function() { + if (isJoined) { + return Promise.reject({ + error: "Internal Server Error", + errcode: "M_WHOOPSIE", + }); + } + return Promise.reject({ + error: "You are not joined", + errcode: "M_FORBIDDEN", + }); + }); + underlyingClient.joinRoom.and.callFake(function(joinRoomId) { + isJoined = true; + return Promise.resolve(joinRoomId); + }); + return intent.sendMessage(roomId, content).catch(function() { + expect(underlyingClient.sendEvent).toHaveBeenCalledWith( + roomId, "m.room.message", content + ); + expect(underlyingClient.joinRoom).toHaveBeenCalledWith(roomId, undefined); + }); + }); + }); + + describe("signaling bridge error", function() { + const reason = "m.event_not_handled" + let affectedUsers, eventId, bridge; + + beforeEach(function() { + intent = new Intent(botIntent, botClient, { + ...alreadyRegistered, + dontCheckPowerLevel: true, + }); + // not interested in joins, so no-op them. + intent.onEvent({ + event_id: "test", + type: "m.room.member", + state_key: userId, + room_id: roomId, + content: { + membership: "join" + } + }); + eventId = "$random:event.id"; + bridge = "International Pidgeon Post"; + affectedUsers = ["@_pidgeonpost_.*:home.server"]; + }); + + it("should send an event", function() { + underlyingClient.sendEvent.and.returnValue(Promise.resolve({ + event_id: "$abra:kadabra" + })); + return intent + .unstableSignalBridgeError(roomId, eventId, bridge, reason, affectedUsers) + .then(() => { + expect(underlyingClient.sendEvent).toHaveBeenCalledWith( + roomId, + "de.nasnotfound.bridge_error", + { + "network_name": bridge, + "reason": reason, + "affected_users": affectedUsers, + "m.relates_to": { + "rel_type": "m.reference", + "event_id": eventId + } + } + ); + expect(underlyingClient.joinRoom).not.toHaveBeenCalled(); + }); + }); + }); }); From 87d73f8ed6707ece7999415ec8bde217c0c05c53 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 9 Aug 2021 16:52:29 +0100 Subject: [PATCH 12/21] changelog --- changelog.d/326.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/326.feature diff --git a/changelog.d/326.feature b/changelog.d/326.feature new file mode 100644 index 00000000..2155ba74 --- /dev/null +++ b/changelog.d/326.feature @@ -0,0 +1,2 @@ +**Breaking**: This library now uses the [matrix-bot-sdk](https://github.com/turt2live/matrix-bot-sdk) for Matrix requests. Previously the bridge used the matrix-js-sdk which +is now deprecated in this release, but can still be accessed via `Intent.getClient()`. \ No newline at end of file From 0f98f329c506cc8d3ee3d8677f5c3f0c14b711b2 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Mon, 9 Aug 2021 18:01:30 +0200 Subject: [PATCH 13/21] Update changelog.d/326.feature --- changelog.d/326.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.d/326.feature b/changelog.d/326.feature index 2155ba74..d3a9a138 100644 --- a/changelog.d/326.feature +++ b/changelog.d/326.feature @@ -1,2 +1,2 @@ -**Breaking**: This library now uses the [matrix-bot-sdk](https://github.com/turt2live/matrix-bot-sdk) for Matrix requests. Previously the bridge used the matrix-js-sdk which -is now deprecated in this release, but can still be accessed via `Intent.getClient()`. \ No newline at end of file +**Breaking**: This library now uses the [matrix-bot-sdk](https://github.com/turt2live/matrix-bot-sdk) for Matrix requests. Previously, the bridge used the matrix-js-sdk which +is now deprecated in this release, but can still be accessed via `Intent.getClient()`. From afba818ac78b2876b96445ae5d16cf042b608037 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 15:29:03 +0100 Subject: [PATCH 14/21] Warn about legacy client once --- src/components/intent.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/intent.ts b/src/components/intent.ts index a499deab..f4758e8a 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -26,7 +26,6 @@ import { ReadStream } from "fs"; import BotSdk, { MatrixProfileInfo, PresenceState } from "matrix-bot-sdk"; const log = Logging.get("Intent"); - export type IntentBackingStore = { getMembership: (roomId: string, userId: string) => UserMembership, getMemberProfile: (roomId: string, userid: string) => MatrixProfileInfo, @@ -97,6 +96,9 @@ export type PowerLevelContent = { type UserProfileKeys = "avatar_url"|"displayname"|null; export class Intent { + + private static getClientWarningFired = false; + private _requestCaches: { profile: ClientRequestCache, roomstate: ClientRequestCache, @@ -274,8 +276,11 @@ export class Intent { if (!this.opts.getJsSdkClient) { throw Error('Legacy client not available'); } - log.warn("Support for the matrix-js-sdk will be going away in a future release." + - "Please update occurances of Intent.getClient() and Intent.client"); + if (!Intent.getClientWarningFired) { + log.warn("Support for the matrix-js-sdk will be going away in a future release." + + "Please update occurances of Intent.getClient() and Intent.client"); + Intent.getClientWarningFired = true; + } this.legacyClient = this.opts.getJsSdkClient(); return this.legacyClient; } From 0e0c8e056d0cf02401cb8fe1012f7b80c63a9ddf Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 15:30:48 +0100 Subject: [PATCH 15/21] Improve warning --- src/components/intent.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/intent.ts b/src/components/intent.ts index f4758e8a..7a3cfdc3 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -23,7 +23,7 @@ import BridgeErrorReason = unstable.BridgeErrorReason; import { APPSERVICE_LOGIN_TYPE, ClientEncryptionSession } from "./encryption"; import Logging from "./logging"; import { ReadStream } from "fs"; -import BotSdk, { MatrixProfileInfo, PresenceState } from "matrix-bot-sdk"; +import BotSdk, { MatrixClient, MatrixProfileInfo, PresenceState } from "matrix-bot-sdk"; const log = Logging.get("Intent"); export type IntentBackingStore = { @@ -251,6 +251,10 @@ export class Intent { }; } + public get matrixClient(): MatrixClient { + return this.botSdkIntent.underlyingClient; + } + /** * Legacy property to access the matrix-js-sdk. * @deprecated Support for the matrix-js-sdk client will be going away in @@ -278,7 +282,8 @@ export class Intent { } if (!Intent.getClientWarningFired) { log.warn("Support for the matrix-js-sdk will be going away in a future release." + - "Please update occurances of Intent.getClient() and Intent.client"); + "Please replace usage of Intent.getClient() and Intent.client with either " + + "Intent functions, or Intent.matrixClient"); Intent.getClientWarningFired = true; } this.legacyClient = this.opts.getJsSdkClient(); From fdcc71ac48685f484cb30a0e427f3fe1b6adaaa1 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 15:31:12 +0100 Subject: [PATCH 16/21] Make it warn noisly --- src/components/intent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/intent.ts b/src/components/intent.ts index 7a3cfdc3..606667b0 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -281,7 +281,7 @@ export class Intent { throw Error('Legacy client not available'); } if (!Intent.getClientWarningFired) { - log.warn("Support for the matrix-js-sdk will be going away in a future release." + + console.warn("Support for the matrix-js-sdk will be going away in a future release." + "Please replace usage of Intent.getClient() and Intent.client with either " + "Intent functions, or Intent.matrixClient"); Intent.getClientWarningFired = true; From ffc62ed386e89e460df1c7a20cbea4351f38244b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 15:32:35 +0100 Subject: [PATCH 17/21] Update src/bridge.ts Co-authored-by: Christian Paul --- src/bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bridge.ts b/src/bridge.ts index 0de4e119..e67f7ebc 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1243,7 +1243,7 @@ export class Bridge { if (!roomId) { // In theory this should never be called, but typescript isn't happy. - throw Error('Expected roomId to be defined'); + throw Error('Expected roomId to be truthy'); } if (!this.opts.disableStores) { From d98b3f6f366ae103540acee6d7807a2746d77b30 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 17:22:12 +0100 Subject: [PATCH 18/21] Fix error codes --- spec/unit/state-lookup.spec.js | 8 ++++---- src/bridge.ts | 7 +++++-- src/components/intent.ts | 9 +++++---- src/components/membership-queue.ts | 14 +++++++------- src/components/room-upgrade-handler.ts | 2 +- src/components/state-lookup.ts | 2 +- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/spec/unit/state-lookup.spec.js b/spec/unit/state-lookup.spec.js index 4ae79c1b..520fb7a1 100644 --- a/spec/unit/state-lookup.spec.js +++ b/spec/unit/state-lookup.spec.js @@ -79,12 +79,12 @@ describe("StateLookup", function() { it("should fail the promise if the HTTP call returns 4xx", function(done) { cli.roomState.and.callFake(function(roomId) { return Promise.reject({ - httpStatus: 403 + statusCode: 403 }); }); lookup.trackRoom("!foo:bar").catch(function(err) { - expect(err.httpStatus).toBe(403); + expect(err.statusCode).toBe(403); done(); }); }); @@ -92,12 +92,12 @@ describe("StateLookup", function() { it("should fail the promise if the HTTP call returns 5xx", function(done) { cli.roomState.and.callFake(function(roomId) { return Promise.reject({ - httpStatus: 500 + statusCode: 500 }); }); lookup.trackRoom("!foo:bar").catch(function(err) { - expect(err.httpStatus).toBe(500); + expect(err.statusCode).toBe(500); done(); }); }); diff --git a/src/bridge.ts b/src/bridge.ts index e67f7ebc..1171665a 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1507,7 +1507,6 @@ export class Bridge { if (content && content.avatar_url) { profile.avatar_url = content.avatar_url; } - console.log("CACHED membership entry!", event.room_id, event.state_key, content.membership); this.membershipCache.setMemberEntry( event.room_id, event.state_key, @@ -1545,7 +1544,11 @@ export class Bridge { const metrics = this.metrics = new PrometheusMetrics(registry); - metrics.registerMatrixSdkMetrics(); + if (!this.botSdkAS) { + throw Error('initalise() not called, cannot listen'); + } + + metrics.registerMatrixSdkMetrics(this.botSdkAS); // TODO(paul): register some bridge-wide standard ones here diff --git a/src/components/intent.ts b/src/components/intent.ts index 606667b0..ac75cb09 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -493,7 +493,7 @@ export class Intent { } } catch (ex) { - if (ex.errcode !== "M_FORBIDDEN") { + if (ex.body.errcode !== "M_FORBIDDEN") { throw ex; } } @@ -828,7 +828,8 @@ export class Intent { return await this.botSdkIntent.underlyingClient.getRoomStateEvent(roomId, eventType, stateKey); } catch (ex) { - if (ex.errcode !== "M_NOT_FOUND" || !returnNull) { + console.log("getStateEvent", ex); + if (ex.body.errcode !== "M_NOT_FOUND" || !returnNull) { throw ex; } } @@ -856,7 +857,7 @@ export class Intent { return false; } catch (ex) { - if (ex.httpStatus == 404) { + if (ex.statusCode == 404) { this.encryptedRooms.set(roomId, false); return false; } @@ -1028,7 +1029,7 @@ export class Intent { mark(roomId, "join"); } catch (ex) { - if (ex.errcode !== "M_FORBIDDEN") { + if (ex.body.errcode !== "M_FORBIDDEN") { throw ex; } try { diff --git a/src/components/membership-queue.ts b/src/components/membership-queue.ts index ae3b022b..5626abeb 100644 --- a/src/components/membership-queue.ts +++ b/src/components/membership-queue.ts @@ -247,11 +247,11 @@ export class MembershipQueue { }); } catch (ex) { - if (ex.errcode || ex.httpStatus) { + if (ex.body.errcode || ex.statusCode) { this.failureReasonCounter?.inc({ type: kickUser ? "kick" : type, - errcode: ex.errcode || "none", - http_status: ex.httpStatus || "none" + errcode: ex.body.errcode || "none", + http_status: ex.statusCode || "none" }); } if (!this.shouldRetry(ex, attempts)) { @@ -266,7 +266,7 @@ export class MembershipQueue { this.opts.actionDelayMs ); log.warn(`${reqIdStr} Failed to ${type} ${roomId}, delaying for ${delay}ms`); - log.debug(`${reqIdStr} Failed with: ${ex.errcode} ${ex.message}`); + log.debug(`${reqIdStr} Failed with: ${ex.body.errcode} ${ex.message}`); await new Promise((r) => setTimeout(r, delay)); this.queueMembership({...item, attempts: attempts + 1}).catch((innerEx) => { log.error(`Failed to handle membership change:`, innerEx); @@ -279,11 +279,11 @@ export class MembershipQueue { } } - private shouldRetry(ex: {code: string; errcode: string; httpStatus: number}, attempts: number): boolean { + private shouldRetry(ex: {body: {code: string; errcode: string;}, statusCode: number}, attempts: number): boolean { return !( attempts === this.opts.maxAttempts || - ex.errcode === "M_FORBIDDEN" || - ex.httpStatus === 403 + ex.body.errcode === "M_FORBIDDEN" || + ex.statusCode === 403 ); } } diff --git a/src/components/room-upgrade-handler.ts b/src/components/room-upgrade-handler.ts index 104f5830..299159e1 100644 --- a/src/components/room-upgrade-handler.ts +++ b/src/components/room-upgrade-handler.ts @@ -107,7 +107,7 @@ export class RoomUpgradeHandler { return true; } catch (ex) { - if (ex.errcode === "M_FORBIDDEN") { + if (ex.body.errcode === "M_FORBIDDEN") { return false; } throw Error("Failed to handle upgrade"); diff --git a/src/components/state-lookup.ts b/src/components/state-lookup.ts index 43b205f5..197969a9 100644 --- a/src/components/state-lookup.ts +++ b/src/components/state-lookup.ts @@ -125,7 +125,7 @@ export class StateLookup { return r; } catch (err) { - if (err.httpStatus >= 400 && err.httpStatus < 600) { // 4xx, 5xx + if (err.statusCode >= 400 && err.statusCode < 600) { // 4xx, 5xx throw err; // don't have permission, don't retry. } // wait a bit then try again From 1d624d0b437eccddc67bde3b96cece482ef91790 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 17:22:32 +0100 Subject: [PATCH 19/21] Fix getJoinedMembers response --- src/components/app-service-bot.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/app-service-bot.ts b/src/components/app-service-bot.ts index 3fbf5e3d..62d351b4 100644 --- a/src/components/app-service-bot.ts +++ b/src/components/app-service-bot.ts @@ -67,7 +67,9 @@ export class AppServiceBot { * @param roomId The room to get a list of joined user IDs in. * @return Resolves to a map of user ID => display_name avatar_url */ - public async getJoinedMembers(roomId: string): Promise<{[userId: string]: MatrixProfileInfo}> { + public async getJoinedMembers( + // eslint-disable-next-line camelcase + roomId: string): Promise> { // Until https://github.com/turt2live/matrix-bot-sdk/pull/138 is merged. // eslint-disable-next-line camelcase const res: Record = From b14eb779ba668f55ed38bf4f60c263ab5018af76 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 10 Aug 2021 17:22:44 +0100 Subject: [PATCH 20/21] Hook up prometheus metrics --- src/components/prometheusmetrics.ts | 107 +++++++++++++--------------- yarn.lock | 6 +- 2 files changed, 51 insertions(+), 62 deletions(-) diff --git a/src/components/prometheusmetrics.ts b/src/components/prometheusmetrics.ts index 730dac2d..84fa29ba 100644 --- a/src/components/prometheusmetrics.ts +++ b/src/components/prometheusmetrics.ts @@ -15,10 +15,11 @@ limitations under the License. import PromClient, { Registry } from "prom-client"; import { AgeCounters } from "./agecounters"; -import JsSdk from "matrix-js-sdk"; import { Request, Response } from "express"; import { Bridge } from ".."; import Logger from "./logging"; +import { Appservice as BotSdkAppservice, FunctionCallContext, METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL, + METRIC_MATRIX_CLIENT_FUNCTION_CALL, METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL } from "matrix-bot-sdk"; type CollectorFunction = () => Promise|void; export interface BridgeGaugesCounts { @@ -119,67 +120,53 @@ export class PrometheusMetrics { * matrix-js-sdk. In particular, a metric is added that counts the number of * calls to client API endpoints made by the client library. */ - public registerMatrixSdkMetrics() { + public registerMatrixSdkMetrics(appservice: BotSdkAppservice): void { const callCounts = this.addCounter({ name: "matrix_api_calls", help: "Count of the number of Matrix client API calls made", labels: ["method"], }); - - /* - * We'll now annotate a bunch of the methods in MatrixClient to keep counts - * of every time they're called. This seems to be neater than trying to - * intercept all HTTP requests and try to intuit what internal method was - * invoked based on the HTTP URL. - * It's kind of messy to do this because we have to maintain a list of - * client SDK method names, but the only other alternative is to hook the - * 'request' function and attempt to parse methods out by inspecting the - * underlying client API HTTP URLs, and that is even messier. So this is - * the lesser of two evils. - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const matrixClientPrototype = (JsSdk as any).MatrixClient.prototype; - - const CLIENT_METHODS = [ - "ban", - "createAlias", - "createRoom", - "getProfileInfo", - "getStateEvent", - "invite", - "joinRoom", - "kick", - "leave", - "register", - "roomState", - "sendEvent", - "sendReceipt", - "sendStateEvent", - "sendTyping", - "setAvatarUrl", - "setDisplayName", - "setPowerLevel", - "setPresence", - "setProfileInfo", - "unban", - "uploadContent", - ]; - - CLIENT_METHODS.forEach(function(method) { - callCounts.inc({method: method}, 0); // initialise the count to zero - - const orig = matrixClientPrototype[method]; - matrixClientPrototype[method] = function(...args: unknown[]) { - callCounts.inc({method: method}); - return orig.apply(this, args); - } + const callCountsFailed = this.addCounter({ + name: "matrix_api_calls_failed", + help: "Count of the number of Matrix client API calls made", + labels: ["method"], }); + + appservice.metrics.registerListener({ + onStartMetric: () => { + // Not used yet. + }, + onEndMetric: () => { + // Not used yet. + }, + onIncrement: (metricName, context) => { + if (metricName === METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL) { + const ctx = context as FunctionCallContext; + callCounts.inc({method: ctx.functionName}); + } + if (metricName === METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL) { + const ctx = context as FunctionCallContext; + callCountsFailed.inc({method: ctx.functionName}); + } + }, + onDecrement: () => { + // Not used yet. + }, + onReset: (metricName) => { + if (metricName === METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL) { + callCounts.reset(); + } + if (metricName === METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL) { + callCountsFailed.reset(); + } + }, + }) } /** * Fetch metrics from all configured collectors */ - public async refresh () { + public async refresh (): Promise { try { await Promise.all(this.collectors.map((f) => f())); } @@ -194,7 +181,8 @@ export class PrometheusMetrics { * @param {BridgeGaugesCallback} counterFunc A function that when invoked * returns the current counts of various items in the bridge. */ - public async registerBridgeGauges (counterFunc: () => Promise|BridgeGaugesCounts) { + public async registerBridgeGauges ( + counterFunc: () => Promise|BridgeGaugesCounts): Promise { const matrixRoomsGauge = this.addGauge({ name: "matrix_configured_rooms", help: "Current count of configured rooms by matrix room ID", @@ -262,7 +250,7 @@ export class PrometheusMetrics { * This function is passed no arguments and is not expected to return anything. * It runs purely to have a side-effect on previously registered gauges. */ - public addCollector (func: CollectorFunction) { + public addCollector (func: CollectorFunction): void { this.collectors.push(func); } @@ -281,7 +269,7 @@ export class PrometheusMetrics { * gauge in order to provide a new value for it. * @return {Gauge} A gauge metric. */ - public addGauge (opts: GagueOpts) { + public addGauge (opts: GagueOpts): PromClient.Gauge { const refresh = opts.refresh; const name = [opts.namespace || "bridge", opts.name].join("_"); @@ -311,7 +299,7 @@ export class PrometheusMetrics { * @param {Array=} opts.labels An optional list of string label names * @return {Counter} A counter metric. */ - public addCounter (opts: CounterOpts) { + public addCounter (opts: CounterOpts): PromClient.Counter { const name = [opts.namespace || "bridge", opts.name].join("_"); const counter = this.counters[opts.name] = @@ -330,7 +318,7 @@ export class PrometheusMetrics { * @param{string} name The name the metric was previously registered as. * @param{Object} labels Optional object containing additional label values. */ - public incCounter (name: string, labels: {[label: string]: string}) { + public incCounter (name: string, labels: {[label: string]: string}): void { if (!this.counters[name]) { throw new Error("Unrecognised counter metric name '" + name + "'"); } @@ -360,7 +348,8 @@ export class PrometheusMetrics { help: opts.help, labelNames: opts.labels || [], registers: [this.register], - buckets: opts.buckets, + // Only apply buckets if defined + ...(opts.buckets !== undefined ? {buckets: opts.buckets} : undefined), }); return timer; } @@ -372,7 +361,7 @@ export class PrometheusMetrics { * @return {function} A function to be called to end the timer and report the * observation. */ - public startTimer(name: string, labels: {[label: string]: string}) { + public startTimer(name: string, labels: {[label: string]: string}): () => void { if (!this.timers[name]) { throw Error("Unrecognised timer metric name '" + name + "'"); } @@ -385,7 +374,7 @@ export class PrometheusMetrics { * containing Express app. * @param {Bridge} bridge The containing Bridge instance. */ - public addAppServicePath(bridge: Bridge) { + public addAppServicePath(bridge: Bridge): void { bridge.addAppServicePath({ method: "GET", path: "/metrics", diff --git a/yarn.lock b/yarn.lock index 14f385ac..25eac354 100644 --- a/yarn.lock +++ b/yarn.lock @@ -335,9 +335,9 @@ integrity sha512-dueRKfaJL4RTtSa7bWeTK1M+VH+Gns73oCgzvYfHZywRCoPSd8EkXBL0mZ9unPTveBn+D9phZBaxuzpwjWkW0g== "@types/node@^12": - version "12.20.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.10.tgz#4dcb8a85a8f1211acafb88d72fafc7e3d2685583" - integrity sha512-TxCmnSSppKBBOzYzPR2BR25YlX5Oay8z2XGwFBInuA/Co0V9xJhLlW4kjbxKtgeNo3NOMbQP1A5Rc03y+XecPw== + version "12.20.19" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.19.tgz#538e61fc220f77ae4a4663c3d8c3cb391365c209" + integrity sha512-niAuZrwrjKck4+XhoCw6AAVQBENHftpXw9F4ryk66fTgYaKQ53R4FI7c9vUGGw5vQis1HKBHDR1gcYI/Bq1xvw== "@types/nopt@^3.0.29": version "3.0.29" From 263bfe749dd82f9a09fcef6be07dc541dab249a7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 16 Aug 2021 10:50:00 +0100 Subject: [PATCH 21/21] more errcode cases --- spec/unit/intent.spec.js | 114 +++++++++++++------------ spec/unit/room-upgrade-handler.spec.js | 4 +- src/components/bridge-info-state.ts | 2 +- src/components/intent.ts | 6 +- src/components/room-upgrade-handler.ts | 2 +- 5 files changed, 65 insertions(+), 63 deletions(-) diff --git a/spec/unit/intent.spec.js b/spec/unit/intent.spec.js index 4ce8531c..f045a008 100644 --- a/spec/unit/intent.spec.js +++ b/spec/unit/intent.spec.js @@ -1,5 +1,7 @@ const { Intent } = require("../.."); +const matrixError = (errcode, error) => Promise.reject({body: {errcode, error}}); + describe("Intent", function() { let intent, botIntent, client, botClient, underlyingClient; const userId = "@alice:bar"; @@ -66,10 +68,10 @@ describe("Intent", function() { it("should fail if the join returned an error other than forbidden", function() { - underlyingClient.joinRoom.and.callFake(() => Promise.reject({ - errcode: "M_YOU_ARE_A_FISH", - error: "you're a fish" - })); + underlyingClient.joinRoom.and.callFake(() => matrixError( + "M_YOU_ARE_A_FISH", + "you're a fish" + )); return intent.join(roomId).catch(function() { expect(underlyingClient.joinRoom).toHaveBeenCalled(); }); @@ -80,10 +82,10 @@ describe("Intent", function() { it("should make the bot invite then the client join", function() { underlyingClient.joinRoom.and.callFake(function() { if (botClient.inviteUser.calls.count() === 0) { - return Promise.reject({ - errcode: "M_FORBIDDEN", - error: "Join first" - }); + return matrixError( + "M_FORBIDDEN", + "Join first" + ); } return Promise.resolve(roomId); }); @@ -103,19 +105,19 @@ describe("Intent", function() { function() { underlyingClient.joinRoom.and.callFake(function() { if (botClient.inviteUser.calls.count() === 0) { - return Promise.reject({ - errcode: "M_FORBIDDEN", - error: "Join first" - }); + return matrixError( + "M_FORBIDDEN", + "Join first" + ); } return Promise.resolve({}); }); botClient.inviteUser.and.callFake(function() { if (botClient.joinRoom.calls.count() === 0) { - return Promise.reject({ - errcode: "M_FORBIDDEN", - error: "Join first" - }); + return matrixError( + "M_FORBIDDEN", + "Join first" + ); } return Promise.resolve({}); }); @@ -135,18 +137,18 @@ describe("Intent", function() { }); it("should give up if the bot cannot join the room", function() { - underlyingClient.joinRoom.and.callFake(() => Promise.reject({ - errcode: "M_FORBIDDEN", - error: "Join first" - })); - botClient.inviteUser.and.callFake(() => Promise.reject({ - errcode: "M_FORBIDDEN", - error: "No invites kthx" - })); - botClient.joinRoom.and.callFake(() => Promise.reject({ - errcode: "M_FORBIDDEN", - error: "No bots allowed!" - })); + underlyingClient.joinRoom.and.callFake(() => matrixError( + "M_FORBIDDEN", + "Join first" + )); + botClient.inviteUser.and.callFake(() => matrixError( + "M_FORBIDDEN", + "No invites kthx" + )); + botClient.joinRoom.and.callFake(() => matrixError( + "M_FORBIDDEN", + "No bots allowed!" + )); return intent.join(roomId).catch(function() { expect(underlyingClient.joinRoom).toHaveBeenCalledWith( @@ -222,10 +224,10 @@ describe("Intent", function() { if (underlyingClient.sendStateEvent.calls.count() > 1) { return Promise.resolve({}); } - return Promise.reject({ - errcode: "M_FORBIDDEN", - error: "Not enough powaaaaaa" - }); + return matrixError( + "M_FORBIDDEN", + "Not enough powaaaaaa", + ); }); botClient.setUserPowerLevel.and.returnValue(Promise.resolve({})); // give the power to the bot @@ -289,10 +291,10 @@ describe("Intent", function() { }); it("should fail if get an error that isn't M_FORBIDDEN", function() { - underlyingClient.sendEvent.and.callFake(() => Promise.reject({ - error: "Oh no", - errcode: "M_UNKNOWN" - })); + underlyingClient.sendEvent.and.callFake(() => matrixError( + "M_UNKNOWN", + "Oh no", + )); return intent.sendMessage(roomId, content).catch(function() { expect(underlyingClient.sendEvent).toHaveBeenCalledWith( roomId, "m.room.message", content @@ -307,10 +309,10 @@ describe("Intent", function() { if (isJoined) { return Promise.resolve("$12345:6789"); } - return Promise.reject({ - error: "You are not joined", - errcode: "M_FORBIDDEN" - }); + return matrixError( + "M_FORBIDDEN", + "You are not joined", + ); }); underlyingClient.joinRoom.and.callFake(function(joinRoomId) { isJoined = true; @@ -327,15 +329,15 @@ describe("Intent", function() { it("should fail if the join on M_FORBIDDEN fails", function() { underlyingClient.sendEvent.and.callFake(function() { - return Promise.reject({ - error: "You are not joined", - errcode: "M_FORBIDDEN" - }); + return matrixError( + "M_FORBIDDEN", + "You are not joined", + ); }); - underlyingClient.joinRoom.and.callFake(() => Promise.reject({ - error: "Never!", - errcode: "M_YOU_ARE_A_FISH" - })); + underlyingClient.joinRoom.and.callFake(() => matrixError( + "M_YOU_ARE_A_FISH", + "Never!", + )); return intent.sendMessage(roomId, content).catch(function() { expect(underlyingClient.sendEvent).toHaveBeenCalledWith( roomId, "m.room.message", content @@ -348,15 +350,15 @@ describe("Intent", function() { let isJoined = false; underlyingClient.sendEvent.and.callFake(function() { if (isJoined) { - return Promise.reject({ - error: "Internal Server Error", - errcode: "M_WHOOPSIE", - }); + return matrixError( + "M_WHOOPSIE", + "Internal Server Error", + ); } - return Promise.reject({ - error: "You are not joined", - errcode: "M_FORBIDDEN", - }); + return matrixError( + "M_FORBIDDEN", + "You are not joined", + ); }); underlyingClient.joinRoom.and.callFake(function(joinRoomId) { isJoined = true; diff --git a/spec/unit/room-upgrade-handler.spec.js b/spec/unit/room-upgrade-handler.spec.js index 69848dfc..4ca26369 100644 --- a/spec/unit/room-upgrade-handler.spec.js +++ b/spec/unit/room-upgrade-handler.spec.js @@ -35,7 +35,7 @@ describe("RoomUpgradeHandler", () => { let joined; const bridge = { getIntent: () => ({ - join: (roomId) => { joined = roomId; return Promise.reject({errcode: "M_FORBIDDEN"}); }, + join: (roomId) => { joined = roomId; return Promise.reject({body: {errcode: "M_FORBIDDEN"}}); }, }), }; const ruh = new RoomUpgradeHandler({}, bridge); @@ -91,7 +91,7 @@ describe("RoomUpgradeHandler", () => { let joined; const bridge = { getIntent: () => ({ - join: (roomId) => { joined = roomId; return Promise.reject({errcode: "M_FORBIDDEN"}); }, + join: (roomId) => { joined = roomId; return Promise.reject({body: {errcode: "M_FORBIDDEN"}}); }, }), }; const ruh = new RoomUpgradeHandler({}, bridge); diff --git a/src/components/bridge-info-state.ts b/src/components/bridge-info-state.ts index bd675f21..db4b83ad 100644 --- a/src/components/bridge-info-state.ts +++ b/src/components/bridge-info-state.ts @@ -91,7 +91,7 @@ export class BridgeInfoStateSyncer { } } catch (ex) { - log.warn(`Encountered error when trying to sync ${roomId}`); + log.warn(`Encountered error when trying to sync ${roomId}`, ex); break; // To be on the safe side, do not retry this room. } diff --git a/src/components/intent.ts b/src/components/intent.ts index ac75cb09..9ae472d5 100644 --- a/src/components/intent.ts +++ b/src/components/intent.ts @@ -968,7 +968,7 @@ export class Intent { return await promiseFn(); } catch (err) { - if (err.errcode !== "M_FORBIDDEN") { + if (err.body?.errcode !== "M_FORBIDDEN") { // not a guardable error throw err; } @@ -1162,11 +1162,11 @@ export class Intent { this.opts.registered = true; } catch (err) { - if (err.errcode === "M_EXCLUSIVE" && this.botClient === this.botSdkIntent.underlyingClient) { + if (err.body?.errcode === "M_EXCLUSIVE" && this.botClient === this.botSdkIntent.underlyingClient) { // Registering the bot will leave it this.opts.registered = true; } - else if (err.errcode === "M_USER_IN_USE") { + else if (err.body?.errcode === "M_USER_IN_USE") { this.opts.registered = true; } else { diff --git a/src/components/room-upgrade-handler.ts b/src/components/room-upgrade-handler.ts index 299159e1..b186b125 100644 --- a/src/components/room-upgrade-handler.ts +++ b/src/components/room-upgrade-handler.ts @@ -107,7 +107,7 @@ export class RoomUpgradeHandler { return true; } catch (ex) { - if (ex.body.errcode === "M_FORBIDDEN") { + if (ex.body?.errcode === "M_FORBIDDEN") { return false; } throw Error("Failed to handle upgrade");