diff --git a/Dockerfile b/Dockerfile index 0aa0e3d56db4bb73ddf843bef55eb838f8fb8f5f..83cecfca0628a4313caf1025f2ff1dc88da3b2be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM node:alpine AS BUILD COPY . /tmp/src # install some dependencies needed for the build process -RUN apk add --no-cache -t build-deps make gcc g++ python ca-certificates libc-dev wget +RUN apk add --no-cache -t build-deps make gcc g++ python ca-certificates libc-dev wget git RUN cd /tmp/src \ && npm install \ && npm run build diff --git a/README.md b/README.md index c1ce1e7afbf55a5c276424fd3423eeafd6b3d411..b98ee36ba88226e0fe7fba2eb60b803ecd1afc39 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ bridging, with one or two bugs cropping up. [](https://travis-ci.org/Half-Shot/matrix-appservice-discord) +[](https://hub.docker.com/r/halfshot/matrix-appservice-discord) [](https://matrix.to/#/#discord:half-shot.uk) ### PRs @@ -30,7 +31,7 @@ The bridge supports any version of Node.js >= v10.X, including all [current rele ### Setup the bridge -* Run ``npm install`` to grab the dependencies. +* Run ``npm install`` to grab the dependencies. `npm` may complain about peer dependencies, but you can safely ignore these. * Run ``npm run build`` to build the typescript into javascript. * Copy ``config/config.sample.yaml`` to ``config.yaml`` and edit it to reflect your setup. * Note that you are expected to set ``domain`` and ``homeserverURL`` to your **public** host name. @@ -79,6 +80,10 @@ docker build -t halfshot/matrix-appservice-discord . # Run the container docker run -v /matrix-appservice-discord:/data -p 9005:9005 halfshot/matrix-appservice-discord ``` +#### Metrics + +The bridge supports reporting metrics via Prometheus. You can configure metrics support in the config +file. The metrics will be reported under the URL provided in the registration file, on the `/metrics` endpoint. #### 3PID Protocol Support @@ -104,6 +109,7 @@ should show up in the network list on Riot and other clients. * For the bot to appear online on Discord you need to run the bridge itself. * ``npm start`` +* Particular configuration keys can be overridden by defining corresponding environment variables. For instance, `auth.botToken` can be set with `APPSERVICE_DISCORD_AUTH_BOT_TOKEN`. [Howto](./docs/howto.md) diff --git a/package-lock.json b/package-lock.json index 1d2bb4100a1651b5ff6c744b0217d9a08d731376..d245fce9e7be738eb720cc3f69e9310aed90910d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "matrix-appservice-discord", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -8,7 +8,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, "requires": { "@babel/highlight": "^7.0.0" } @@ -67,7 +66,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, "requires": { "chalk": "^2.0.0", "esutils": "^2.0.2", @@ -77,8 +75,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" } } }, @@ -190,11 +187,6 @@ "integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg==", "dev": true }, - "@types/p-queue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/p-queue/-/p-queue-3.0.0.tgz", - "integrity": "sha512-brMFTEZiltJnAxNy07LzRVw2bdQX1ElCElHEmo5WEI70oWQe00aXew9RKmpf8MOzBMiMfNeuQoEyQCWKawPzmQ==" - }, "@types/sqlite3": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/sqlite3/-/sqlite3-3.1.3.tgz", @@ -466,6 +458,33 @@ "tar": "^4.4.6" } }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, + "bluebird": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", + "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -497,8 +516,6 @@ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" }, -<<<<<<< HEAD -======= "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -516,7 +533,6 @@ "write-file-atomic": "^2.4.2" } }, ->>>>>>> develop "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -698,9 +714,9 @@ } }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" } @@ -755,9 +771,12 @@ } }, "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } }, "content-type": { "version": "1.0.4", @@ -774,9 +793,9 @@ } }, "cookie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", - "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" }, "cookie-signature": { "version": "1.0.6", @@ -947,15 +966,15 @@ } }, "discord.js": { - "version": "11.4.2", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.4.2.tgz", - "integrity": "sha512-MDwpu0lMFTjqomijDl1Ed9miMQe6kB4ifKdP28QZllmLv/HVOJXhatRgjS8urp/wBlOfx+qAYSXcdI5cKGYsfg==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-11.5.1.tgz", + "integrity": "sha512-tGhV5xaZXE3Z+4uXJb3hYM6gQ1NmnSxp9PClcsSAYFVRzH6AJH74040mO3afPDMWEAlj8XsoPXXTJHTxesqcGw==", "requires": { "long": "^4.0.0", "prism-media": "^0.0.3", "snekfetch": "^3.6.4", "tweetnacl": "^1.0.0", - "ws": "^4.0.0" + "ws": "^6.0.0" } }, "doctrine": { @@ -1031,12 +1050,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, -<<<<<<< HEAD - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" -======= "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -1045,7 +1058,11 @@ "requires": { "once": "^1.4.0" } ->>>>>>> develop + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" }, "env-variable": { "version": "0.0.5", @@ -1361,109 +1378,40 @@ "dev": true }, "express": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", - "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "requires": { - "accepts": "~1.3.5", + "accepts": "~1.3.7", "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", "content-type": "~1.0.4", - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.1.1", + "finalhandler": "~1.1.2", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" - }, - "dependencies": { - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "requires": { - "bytes": "3.0.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "~1.6.3", - "iconv-lite": "0.4.23", - "on-finished": "~2.3.0", - "qs": "6.5.2", - "raw-body": "2.3.3", - "type-is": "~1.6.16" - } - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "raw-body": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", - "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - } - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } } }, "extend": { @@ -1541,24 +1489,17 @@ } }, "finalhandler": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.4.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", "unpipe": "~1.0.0" - }, - "dependencies": { - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" - } } }, "find-cache-dir": { @@ -1792,7 +1733,6 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, -<<<<<<< HEAD "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", @@ -1800,7 +1740,8 @@ "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" -======= + } + }, "hasha": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", @@ -1808,7 +1749,6 @@ "dev": true, "requires": { "is-stream": "^1.0.1" ->>>>>>> develop } }, "he": { @@ -1821,7 +1761,17 @@ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.13.1.tgz", "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==" }, -<<<<<<< HEAD + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "htmlencode": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/htmlencode/-/htmlencode-0.0.4.tgz", + "integrity": "sha1-9+LWr74YqHp45jujMI51N2Z0Dj8=" + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -1836,21 +1786,16 @@ }, "dependencies": { "readable-stream": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", - "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } } -======= - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", - "dev": true + } }, "http-errors": { "version": "1.7.2", @@ -1862,7 +1807,6 @@ "setprototypeof": "1.1.1", "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" ->>>>>>> develop } }, "http-signature": { @@ -1875,6 +1819,14 @@ "sshpk": "^1.7.0" } }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ignore": { "version": "3.3.10", "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", @@ -2372,16 +2324,6 @@ "type-check": "~0.3.2" } }, -<<<<<<< HEAD -======= - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", - "requires": { - "immediate": "~3.0.5" - } - }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -2402,14 +2344,6 @@ } } }, - "localforage": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz", - "integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==", - "requires": { - "lie": "3.1.1" - } - }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -2420,13 +2354,11 @@ "path-exists": "^3.0.0" } }, ->>>>>>> develop "lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" }, -<<<<<<< HEAD "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -2437,6 +2369,12 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -2451,13 +2389,6 @@ "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==" -======= - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true ->>>>>>> develop }, "lodash.padend": { "version": "4.6.1", @@ -2488,7 +2419,6 @@ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" }, -<<<<<<< HEAD "lowdb": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-1.0.0.tgz", @@ -2508,7 +2438,6 @@ } } }, -======= "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -2551,15 +2480,11 @@ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", "dev": true }, ->>>>>>> develop "manakin": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/manakin/-/manakin-0.5.2.tgz", "integrity": "sha512-pfDSB7QYoVg0Io4KMV9hhPoXpj6p0uBscgtyUSKCOFZe8bqgbpStfgnKIbF/ulnr6U3ICu4OqdyxAqBgOhZwBQ==" }, -<<<<<<< HEAD - "matrix-bot-sdk": { -======= "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -2569,73 +2494,53 @@ "p-defer": "^1.0.0" } }, - "matrix-appservice": { ->>>>>>> develop - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/matrix-bot-sdk/-/matrix-bot-sdk-0.3.5.tgz", - "integrity": "sha512-/FnQ+bbZcTfAHGBPVX9tBezLR3D7omprmLYCyf2XvGkXaUqUbW/JKfILGEyIbzSPPrWcqm+vn8VGGGBldevDRQ==", + "matrix-bot-sdk": { + "version": "github:turt2live/matrix-js-bot-sdk#0c3d9bcde0157a638fa4c3573d6201e4d56d2a1e", + "from": "github:turt2live/matrix-js-bot-sdk#develop", "requires": { - "@types/node": "^8.10.34", - "bluebird": "^3.5.2", - "express": "^4.16.4", + "@types/node": "^10.14.9", + "bluebird": "^3.5.5", + "express": "^4.17.1", "hash.js": "^1.1.7", + "htmlencode": "0.0.4", "lowdb": "^1.0.0", "morgan": "^1.9.1", -<<<<<<< HEAD -======= - "request": "^2.88.0" - } - }, - "matrix-appservice-bridge": { - "version": "github:matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76", - "from": "github:matrix-org/matrix-appservice-bridge#8a7288edf1d1d1d1395a83d330d836d9c9bf1e76", - "requires": { - "bluebird": "^2.9.34", - "chalk": "^2.4.1", - "extend": "^3.0.0", - "is-my-json-valid": "^2.19.0", - "js-yaml": "^3.12.0", - "matrix-appservice": "0.3.5", - "matrix-js-sdk": "^1.0.1", - "nedb": "^1.1.3", - "nopt": "^3.0.3", - "prom-client": "^11.1.1", - "request": "^2.61.0", - "winston": "^3.1.0", - "winston-daily-rotate-file": "^3.3.3" - } - }, - "matrix-js-sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/matrix-js-sdk/-/matrix-js-sdk-1.1.0.tgz", - "integrity": "sha512-ECoMN6DkwPdKiMa/jSoMkSDngFCo6x7oH84rLd1NtD7lBPl3Ejj6ARa0iIELE7u0OUO6J0FzdWh7Hd0ZnVTmww==", - "requires": { - "another-json": "^0.2.0", - "babel-runtime": "^6.26.0", - "base-x": "3.0.4", - "bluebird": "^3.5.0", - "browser-request": "^0.3.3", - "bs58": "^4.0.1", - "content-type": "^1.0.2", - "loglevel": "1.6.1", - "qs": "^6.5.2", ->>>>>>> develop "request": "^2.88.0", - "request-promise": "^4.2.2", - "sanitize-html": "^1.20.0", - "tslint": "^5.11.0", - "typescript": "^3.1.1" + "request-promise": "^4.2.4", + "sanitize-html": "^1.20.1", + "tslint": "^5.17.0", + "typescript": "^3.5.2" }, "dependencies": { "@types/node": { - "version": "8.10.48", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.48.tgz", - "integrity": "sha512-c35YEBTkL4rzXY2ucpSKy+UYHjUBIIkuJbWYbsGIrKLEWU5dgJMmLkkIb3qeC3O3Tpb1ZQCwecscvJTDjDjkRw==" + "version": "10.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.9.tgz", + "integrity": "sha512-NelG/dSahlXYtSoVPErrp06tYFrvzj8XLWmKA+X8x0W//4MqbUyZu++giUG/v0bjAT6/Qxa8IjodrfdACyb0Fg==" }, - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" + "tslint": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.17.0.tgz", + "integrity": "sha512-pflx87WfVoYepTet3xLfDOLDm9Jqi61UXIKePOuca0qoAZyrGWonDG9VTbji58Fy+8gciUn8Bt7y69+KEVjc/w==", + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + } + }, + "typescript": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", + "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==" } } }, @@ -2700,18 +2605,16 @@ "mime-db": "1.40.0" } }, -<<<<<<< HEAD - "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==" -======= "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true ->>>>>>> develop + }, + "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", @@ -3116,11 +3019,6 @@ "p-limit": "^2.0.0" } }, - "p-queue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-3.0.0.tgz", - "integrity": "sha512-2tv/MRmPXfmfnjLLJAHl+DdU8p2DhZafAnlpm/C/T5BpF5L9wKz5tMin4A4N2zVpJL2YMhPlRmtO7s5EtNrjfA==" - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -3317,9 +3215,9 @@ "dev": true }, "postcss": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.15.tgz", - "integrity": "sha512-+avadY713SyQf0m5np7byFzAFZyhPhXyxwp8OVmdd5mKOxm0VzM2AJkKIgBro7gGVk4kYlCDvBVrSqhU5m8E+w==", + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.17.tgz", + "integrity": "sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==", "requires": { "chalk": "^2.4.2", "source-map": "^0.6.1", @@ -3406,6 +3304,14 @@ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true }, + "prom-client": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.1.tgz", + "integrity": "sha512-AcFuxVgzoA/4nlpeg9SkM2HkDjNU3V7g2LCLwpudXSbcSLiFpRMVfsCoCY5RYeR/d9jkQng1mCmVKj1mPHvP0Q==", + "requires": { + "tdigest": "^0.1.1" + } + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -3441,9 +3347,9 @@ "dev": true }, "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" }, "pump": { "version": "3.0.0", @@ -3460,13 +3366,16 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, -<<<<<<< HEAD -======= "raw-body": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", @@ -3499,7 +3408,6 @@ "read-pkg": "^3.0.0" } }, ->>>>>>> develop "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", @@ -3539,13 +3447,6 @@ "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-1.0.1.tgz", "integrity": "sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=" }, -<<<<<<< HEAD -======= - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, "release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -3555,7 +3456,6 @@ "es6-error": "^4.0.1" } }, ->>>>>>> develop "request": { "version": "2.88.0", "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", @@ -3590,7 +3490,6 @@ } } }, -<<<<<<< HEAD "request-promise": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.4.tgz", @@ -3600,13 +3499,6 @@ "request-promise-core": "1.1.2", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" - }, - "dependencies": { - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==" - } } }, "request-promise-core": { @@ -3616,7 +3508,7 @@ "requires": { "lodash": "^4.17.11" } -======= + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3628,7 +3520,6 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true ->>>>>>> develop }, "require-uncached": { "version": "1.0.3", @@ -3721,9 +3612,9 @@ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "requires": { "debug": "2.6.9", "depd": "~1.1.2", @@ -3732,55 +3623,32 @@ "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "~1.6.2", - "mime": "1.4.1", - "ms": "2.0.0", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", "on-finished": "~2.3.0", - "range-parser": "~1.2.0", - "statuses": "~1.4.0" + "range-parser": "~1.2.1", + "statuses": "~1.5.0" }, "dependencies": { - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "mime": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", - "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" - }, - "statuses": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", - "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" } } }, "serve-static": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", - "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "requires": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.2" + "parseurl": "~1.3.3", + "send": "0.17.1" } }, -<<<<<<< HEAD -======= "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -3807,7 +3675,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, ->>>>>>> develop "shelljs": { "version": "0.7.8", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", @@ -3984,6 +3851,11 @@ "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", "dev": true }, + "statuses": { + "version": "1.5.0", + "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", @@ -4160,8 +4032,6 @@ "yallist": "^3.0.2" } }, -<<<<<<< HEAD -======= "tdigest": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", @@ -4182,7 +4052,6 @@ "require-main-filename": "^2.0.0" } }, ->>>>>>> develop "test-value": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/test-value/-/test-value-2.1.0.tgz", @@ -4218,8 +4087,6 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, -<<<<<<< HEAD -======= "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -4231,7 +4098,6 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, ->>>>>>> develop "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -4313,9 +4179,9 @@ } }, "tweetnacl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.0.tgz", - "integrity": "sha1-cT2LgY2kIGh0C/aDhtBHnmb8ins=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", + "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==" }, "type-check": { "version": "0.3.2", @@ -4614,12 +4480,11 @@ } }, "ws": { - "version": "4.1.0", - "resolved": "http://registry.npmjs.org/ws/-/ws-4.1.0.tgz", - "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", + "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", "requires": { - "async-limiter": "~1.0.0", - "safe-buffer": "~5.1.0" + "async-limiter": "~1.0.0" } }, "xtend": { diff --git a/package.json b/package.json index 0dcafcb053dc8768043fc5356de5b2acd5a472a1..aff9171452cf8e804bc12a47472bcfeb7e8b14c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-appservice-discord", - "version": "0.4.0", + "version": "0.5.0", "description": "A bridge between Matrix and Discord", "main": "discordas.js", "scripts": { @@ -36,21 +36,19 @@ "dependencies": { "@types/command-line-args": "^5.0.0", "@types/js-yaml": "^3.12.1", - "@types/p-queue": "^3.0.0", "better-sqlite3": "^5.0.1", "command-line-args": "^4.0.7", "command-line-usage": "^4.1.0", "discord-markdown": "^2.0.0", - "discord.js": "^11.4.2", + "discord.js": "^11.5.1", "escape-html": "^1.0.3", "escape-string-regexp": "^1.0.5", "js-yaml": "^3.13.1", - "matrix-bot-sdk": "^0.3.5", + "matrix-bot-sdk": "turt2live/matrix-js-bot-sdk#develop", "mime": "^1.6.0", - "moment": "^2.22.2", "node-html-parser": "^1.1.11", - "p-queue": "^3.0.0", "pg-promise": "^8.5.1", + "prom-client": "^11.3.0", "tslint": "^5.11.0", "typescript": "^3.1.3", "winston": "^3.0.0", diff --git a/src/bot.ts b/src/bot.ts index 22c180ae0b6febaf21cb0e357bbd86557dea4b58..23457ab9dc0c631b03a607da97f3712505020ab5 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -37,6 +37,7 @@ import * as mime from "mime"; import { IMatrixEvent, IMatrixMediaInfo } from "./matrixtypes"; import { Appservice, Intent } from "matrix-bot-sdk"; import { DiscordCommandHandler } from "./discordcommandhandler"; +import { MetricPeg } from "./metrics"; const log = new Log("DiscordBot"); @@ -81,8 +82,8 @@ export class DiscordBot { /* Handles messages queued up to be sent to matrix from discord. */ private discordMessageQueue: { [channelId: string]: Promise<void> }; - private channelLocks: { [channelId: string]: {p: Promise<{}>, i: NodeJS.Timeout|null} }; - + private channelLocks: Map<string, {i: NodeJS.Timeout|null, r: (() => void)|null}>; + private channelLockPromises: Map<string, Promise<{}>>; constructor( private config: DiscordBridgeConfig, private bridge: Appservice, @@ -105,7 +106,8 @@ export class DiscordBot { // init vars this.sentMessages = []; this.discordMessageQueue = {}; - this.channelLocks = {}; + this.channelLocks = new Map(); + this.channelLockPromises = new Map(); this.lastEventIds = {}; } @@ -138,34 +140,39 @@ export class DiscordBot { } public lockChannel(channel: Discord.Channel) { - if (this.channelLocks[channel.id]) { + if (this.channelLocks.has(channel.id)) { return; } - const p = new Promise((resolve) => { - if (!this.channelLocks[channel.id]) { - resolve(); - return; - } - const i = setInterval(resolve, this.config.limits.discordSendDelay); - this.channelLocks[channel.id].i = i; + this.channelLocks[channel.id] = {i: null, r: null, p: null}; + const p = new Promise<{}>((resolve) => { + const i = setTimeout(() => { + log.warn(`Lock on channel ${channel.id} expired. Discord is lagging behind?`); + this.unlockChannel(channel); + }, this.config.limits.discordSendDelay); + const o = Object.assign({r: resolve, i, p: null}, this.channelLocks.get(channel.id) || {}); + this.channelLocks.set(channel.id, o); }); - - this.channelLocks[channel.id] = {i: null, p}; + this.channelLockPromises.set(channel.id, p); } public unlockChannel(channel: Discord.Channel) { - const lock = this.channelLocks[channel.id]; - if (lock && lock.i !== null) { + if (!this.channelLocks.has(channel.id)) { + return; + } + const lock = this.channelLocks.get(channel.id)!; + if (lock.i !== null) { + lock.r!(); clearTimeout(lock.i); } - delete this.channelLocks[channel.id]; + this.channelLocks.delete(channel.id); + this.channelLockPromises.delete(channel.id); } public async waitUnlock(channel: Discord.Channel) { - const lock = this.channelLocks[channel.id]; - if (lock) { - await lock.p; + const promise = this.channelLockPromises.get(channel.id); + if (promise) { + await promise; } } @@ -233,6 +240,7 @@ export class DiscordBot { client.on("messageDelete", async (msg: Discord.Message) => { try { await this.waitUnlock(msg.channel); + this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel); this.discordMessageQueue[msg.channel.id] = (async () => { await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); try { @@ -253,6 +261,7 @@ export class DiscordBot { promiseArr.push(async () => { try { await this.waitUnlock(msg.channel); + this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel); await this.DeleteDiscordMessage(msg); } catch (err) { log.error("Caught while handling 'messageDeleteBulk'", err); @@ -267,6 +276,7 @@ export class DiscordBot { client.on("messageUpdate", async (oldMessage: Discord.Message, newMessage: Discord.Message) => { try { await this.waitUnlock(newMessage.channel); + this.clientFactory.bindMetricsToChannel(newMessage.channel as Discord.TextChannel); this.discordMessageQueue[newMessage.channel.id] = (async () => { await (this.discordMessageQueue[newMessage.channel.id] || Promise.resolve()); try { @@ -281,12 +291,15 @@ export class DiscordBot { }); client.on("message", async (msg: Discord.Message) => { try { + MetricPeg.get.registerRequest(msg.id); await this.waitUnlock(msg.channel); + this.clientFactory.bindMetricsToChannel(msg.channel as Discord.TextChannel); this.discordMessageQueue[msg.channel.id] = (async () => { await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); try { await this.OnMessage(msg); } catch (err) { + MetricPeg.get.requestOutcome(msg.id, true, "fail"); log.error("Caught while handing 'message'", err); } })(); @@ -382,6 +395,7 @@ export class DiscordBot { } const channel = guild.channels.get(room); if (channel && channel.type === "text") { + this.ClientFactory.bindMetricsToChannel(channel as Discord.TextChannel); const lookupResult = new ChannelLookupResult(); lookupResult.channel = channel as Discord.TextChannel; lookupResult.botUser = this.bot.user.id === client.user.id; @@ -438,9 +452,10 @@ export class DiscordBot { try { this.lockChannel(chan); if (!botUser) { - opts.embed = embedSet.replyEmbed; + // NOTE: Don't send replies to discord if we are a puppet. msg = await chan.send(embed.description, opts); } else if (hook) { + MetricPeg.get.remoteCall("hook.send"); msg = await hook.send(embed.description, { avatarURL: embed!.author!.icon_url, embeds: embedSet.replyEmbed ? [embedSet.replyEmbed] : undefined, @@ -455,8 +470,13 @@ export class DiscordBot { opts.embed = embed; msg = await chan.send("", opts); } - await this.StoreMessagesSent(msg, chan, event); - this.unlockChannel(chan); + // Don't block on this. + this.StoreMessagesSent(msg, chan, event).then(() => { + this.unlockChannel(chan); + }).catch(() => { + log.warn("Failed to store sent message for ", event.event_id); + }); + } catch (err) { log.error("Couldn't send message. ", err); } @@ -532,6 +552,7 @@ export class DiscordBot { if (guild) { const channel = client.channels.get(entry.remote!.get("discord_channel") as string); if (channel) { + this.ClientFactory.bindMetricsToChannel(channel as Discord.TextChannel); return channel; } throw Error("Channel given in room entry not found"); @@ -733,12 +754,14 @@ export class DiscordBot { if (indexOfMsg !== -1) { log.verbose("Got repeated message, ignoring."); delete this.sentMessages[indexOfMsg]; + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; // Skip *our* messages } const chan = msg.channel as Discord.TextChannel; if (msg.author.id === this.bot.user.id) { // We don't support double bridging. log.verbose("Not reflecting bot's own messages"); + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; } // Test for webhooks @@ -748,6 +771,8 @@ export class DiscordBot { if (webhook && msg.webhookID === webhook.id) { // Filter out our own webhook messages. log.verbose("Not reflecting own webhook messages"); + // Filter out our own webhook messages. + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; } } @@ -755,6 +780,7 @@ export class DiscordBot { // check if it is a command to process by the bot itself if (msg.content.startsWith("!matrix")) { await this.discordCommandHandler.Process(msg); + MetricPeg.get.requestOutcome(msg.id, true, "success"); return; } @@ -763,14 +789,15 @@ export class DiscordBot { let rooms; try { rooms = await this.channelSync.GetRoomIdsFromChannel(msg.channel); + if (rooms === null) { + throw Error(); + } } catch (err) { log.verbose("No bridged rooms to send message to. Oh well."); + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return null; } try { - if (rooms === null) { - return null; - } const intent = this.GetIntentFromDiscordMember(msg.author, msg.webhookID); // Check Attachements await Util.AsyncForEach(msg.attachments.array(), async (attachment) => { @@ -856,7 +883,9 @@ export class DiscordBot { await afterSend(res); } }); + MetricPeg.get.requestOutcome(msg.id, true, "success"); } catch (err) { + MetricPeg.get.requestOutcome(msg.id, true, "fail"); log.verbose("Failed to send message into room.", err); } } diff --git a/src/channelsyncroniser.ts b/src/channelsyncroniser.ts index 1bb5817588e938bdc51bc6be9b0ce67597d7d224..ee1cb85bfbc66198690aecbefaf60944686a286b 100644 --- a/src/channelsyncroniser.ts +++ b/src/channelsyncroniser.ts @@ -210,7 +210,9 @@ export class ChannelSyncroniser { const icon = channel.guild.icon; let iconUrl: string | null = null; if (icon) { - iconUrl = `https://cdn.discordapp.com/icons/${channel.guild.id}/${icon}.png`; + // if discord prefixes their icon hashes with "a_" it means that they are animated + const animatedIcon = icon.startsWith("a_"); + iconUrl = `https://cdn.discordapp.com/icons/${channel.guild.id}/${icon}.${animatedIcon ? "gif" : "png"}`; } remoteRooms.forEach((remoteRoom) => { const mxid = remoteRoom.matrix!.getId(); diff --git a/src/clientfactory.ts b/src/clientfactory.ts index ae5f930c96221c627ee3d2413f8ce5e0c02574d9..1b2adcfb706231d74d0667c0612962b2208d55c6 100644 --- a/src/clientfactory.ts +++ b/src/clientfactory.ts @@ -16,8 +16,9 @@ limitations under the License. import { DiscordBridgeConfigAuth } from "./config"; import { DiscordStore } from "./store"; -import { Client as DiscordClient } from "discord.js"; +import { Client as DiscordClient, TextChannel } from "discord.js"; import { Log } from "./log"; +import { MetricPeg } from "./metrics"; const log = new Log("ClientFactory"); @@ -107,4 +108,19 @@ export class DiscordClientFactory { return this.botClient; } } + + public bindMetricsToChannel(channel: TextChannel) { + // tslint:disable-next-line:no-any + const flexChan = channel as any; + if (flexChan._xmet_send !== undefined) { + return; + } + // Prefix the real functions with _xmet_ + flexChan._xmet_send = channel.send; + // tslint:disable-next-line:only-arrow-functions + channel.send = function() { + MetricPeg.get.remoteCall("channel.send"); + return flexChan._xmet_send.apply(channel, arguments); + }; + } } diff --git a/src/config.ts b/src/config.ts index 3c1ed9cc2ae83eb5dc711ec1c28e513c611dc08a..94c5a91fc34037fe663c86226b58ab61f13e5b80 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +const ENV_PREFIX = "APPSERVICE_DISCORD"; +const ENV_KEY_SEPARATOR = "_"; +const ENV_VAL_SEPARATOR = ","; + /** Type annotations for config/config.schema.yaml */ export class DiscordBridgeConfig { public bridge: DiscordBridgeConfigBridge = new DiscordBridgeConfigBridge(); @@ -27,18 +31,48 @@ export class DiscordBridgeConfig { /** * Apply a set of keys and values over the default config. - * @param _config Config keys + * @param newConfig Config keys * @param configLayer Private parameter */ // tslint:disable-next-line no-any - public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) { + public applyConfig(newConfig: {[key: string]: any}, configLayer: {[key: string]: any} = this) { Object.keys(newConfig).forEach((key) => { - if ( typeof(configLayer[key]) === "object" && - !Array.isArray(configLayer[key])) { - this.ApplyConfig(newConfig[key], this[key]); - return; + if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) { + this.applyConfig(newConfig[key], configLayer[key]); + } else { + configLayer[key] = newConfig[key]; + } + }); + } + + /** + * Override configuration keys defined in the supplied environment dictionary. + * @param environment environment variable dictionary + * @param path private parameter: config layer path determining the environment key prefix + * @param configLayer private parameter: current layer of configuration to alter recursively + */ + public applyEnvironmentOverrides( + // tslint:disable-next-line no-any + environment: {[key: string]: any}, + path: string[] = [ENV_PREFIX], + // tslint:disable-next-line no-any + configLayer: {[key: string]: any} = this, + ) { + Object.keys(configLayer).forEach((key) => { + // camelCase to THICK_SNAKE + const attributeKey = key.replace(/[A-Z]/g, (prefix) => `${ENV_KEY_SEPARATOR}${prefix}`).toUpperCase(); + const attributePath = path.concat([attributeKey]); + + if (configLayer[key] instanceof Object && !(configLayer[key] instanceof Array)) { + this.applyEnvironmentOverrides(environment, attributePath, configLayer[key]); + } else { + const lookupKey = attributePath.join(ENV_KEY_SEPARATOR); + if (lookupKey in environment) { + configLayer[key] = (configLayer[key] instanceof Array) + ? environment[lookupKey].split(ENV_VAL_SEPARATOR) + : environment[lookupKey]; + } } - configLayer[key] = newConfig[key]; }); } } @@ -58,6 +92,7 @@ class DiscordBridgeConfigBridge { public disableEveryoneMention: boolean = false; public disableHereMention: boolean = false; public disableJoinLeaveNotifications: boolean = false; + public enableMetrics: boolean = false; } export class DiscordBridgeConfigDatabase { diff --git a/src/db/roomstore.ts b/src/db/roomstore.ts index c04230e0fa56c75046a628935f598454b7c6fc7c..ed311a8ca450258f1aeb83fd2d7fb33ba37cc293 100644 --- a/src/db/roomstore.ts +++ b/src/db/roomstore.ts @@ -18,7 +18,8 @@ import { IDatabaseConnector } from "./connector"; import { Util } from "../util"; import * as uuid from "uuid/v4"; -import { Postgres } from "./postgres"; +import { MetricPeg } from "../metrics"; +import { TimedCache } from "../structures/timedcache"; const log = new Log("DbRoomStore"); @@ -92,9 +93,9 @@ const ENTRY_CACHE_LIMETIME = 30000; // XXX: This only implements functions used in the bridge at the moment. export class DbRoomStore { - private entriesMatrixIdCache: Map<string, {e: IRoomStoreEntry[], ts: number}>; + private entriesMatrixIdCache: TimedCache<string, IRoomStoreEntry[]>; constructor(private db: IDatabaseConnector) { - this.entriesMatrixIdCache = new Map(); + this.entriesMatrixIdCache = new TimedCache(ENTRY_CACHE_LIMETIME); } public async upsertEntry(entry: IRoomStoreEntry) { @@ -154,9 +155,11 @@ export class DbRoomStore { public async getEntriesByMatrixId(matrixId: string): Promise<IRoomStoreEntry[]> { const cached = this.entriesMatrixIdCache.get(matrixId); - if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) { - return cached.e; + if (cached) { + MetricPeg.get.storeCall("RoomStore.getEntriesByMatrixId", true); + return cached; } + MetricPeg.get.storeCall("RoomStore.getEntriesByMatrixId", false); const entries = await this.db.All( "SELECT * FROM room_entries WHERE matrix_id = $id", {id: matrixId}, ); @@ -184,12 +187,13 @@ export class DbRoomStore { } } if (res.length > 0) { - this.entriesMatrixIdCache.set(matrixId, {e: res, ts: Date.now()}); + this.entriesMatrixIdCache.set(matrixId, res); } return res; } public async getEntriesByMatrixIds(matrixIds: string[]): Promise<IRoomStoreEntry[]> { + MetricPeg.get.storeCall("RoomStore.getEntriesByMatrixIds", false); const mxIdMap = { }; matrixIds.forEach((mxId, i) => mxIdMap[i] = mxId); const sql = `SELECT * FROM room_entries WHERE matrix_id IN (${matrixIds.map((_, id) => `\$${id}`).join(", ")})`; @@ -222,6 +226,7 @@ export class DbRoomStore { } public async linkRooms(matrixRoom: MatrixStoreRoom, remoteRoom: RemoteStoreRoom) { + MetricPeg.get.storeCall("RoomStore.linkRooms", false); await this.upsertRoom(remoteRoom); const values = { @@ -244,6 +249,7 @@ export class DbRoomStore { } public async getEntriesByRemoteRoomData(data: IRemoteRoomDataLazy): Promise<IRoomStoreEntry[]> { + MetricPeg.get.storeCall("RoomStore.getEntriesByRemoteRoomData", false); Object.keys(data).filter((k) => typeof(data[k]) === "boolean").forEach((k) => { data[k] = Number(data[k]); }); @@ -270,11 +276,13 @@ export class DbRoomStore { } public async removeEntriesByRemoteRoomId(remoteId: string) { + MetricPeg.get.storeCall("RoomStore.removeEntriesByRemoteRoomId", false); await this.db.Run(`DELETE FROM room_entries WHERE remote_id = $remoteId`, {remoteId}); await this.db.Run(`DELETE FROM remote_room_data WHERE room_id = $remoteId`, {remoteId}); } public async removeEntriesByMatrixRoomId(matrixId: string) { + MetricPeg.get.storeCall("RoomStore.removeEntriesByMatrixRoomId", false); const entries = (await this.db.All(`SELECT * FROM room_entries WHERE matrix_id = $matrixId`, {matrixId})) || []; await Util.AsyncForEach(entries, async (entry) => { if (entry.remote_id) { @@ -286,6 +294,7 @@ export class DbRoomStore { } private async upsertRoom(room: RemoteStoreRoom) { + MetricPeg.get.storeCall("RoomStore.upsertRoom", false); if (!room.data) { throw new Error("Tried to upsert a room with undefined data"); } diff --git a/src/db/schema/v7.ts b/src/db/schema/v7.ts index 879b34e1ecde331c5839f8275b58a301dfc0336b..9caaa668a09e008b62f2ae97d2d03921097dfa01 100644 --- a/src/db/schema/v7.ts +++ b/src/db/schema/v7.ts @@ -29,8 +29,8 @@ export class Schema implements IDbSchema { name TEXT NOT NULL, animated INTEGER NOT NULL, mxc_url TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, PRIMARY KEY(emoji_id) );`, "emoji"); diff --git a/src/db/schema/v9.ts b/src/db/schema/v9.ts index dbb111d44aa022875fff72413f670203659bcf1a..abb63e4448d4f3dd5c4d6b56e8a2973d5e5d296c 100644 --- a/src/db/schema/v9.ts +++ b/src/db/schema/v9.ts @@ -17,9 +17,6 @@ limitations under the License. import { IDbSchema } from "./dbschema"; import { DiscordStore } from "../../store"; import { Log } from "../../log"; -import { RemoteUser } from "../userstore"; -import * as Queue from "p-queue"; -const log = new Log("SchemaV9"); export class Schema implements IDbSchema { public description = "create user store tables"; diff --git a/src/db/userstore.ts b/src/db/userstore.ts index cb1251e28c1f08d86e74ba47aa7695da00319474..7e84dc0a84faf4767a188349e11236ef34586d9c 100644 --- a/src/db/userstore.ts +++ b/src/db/userstore.ts @@ -15,8 +15,9 @@ limitations under the License. */ import { IDatabaseConnector } from "./connector"; -import * as uuid from "uuid/v4"; import { Log } from "../log"; +import { MetricPeg } from "../metrics"; +import { TimedCache } from "../structures/timedcache"; /** * A UserStore compatible with @@ -45,17 +46,20 @@ export interface IUserStoreEntry { } export class DbUserStore { - private remoteUserCache: Map<string, {e: RemoteUser, ts: number}>; + private remoteUserCache: TimedCache<string, RemoteUser>; constructor(private db: IDatabaseConnector) { - this.remoteUserCache = new Map(); + this.remoteUserCache = new TimedCache(ENTRY_CACHE_LIMETIME); } public async getRemoteUser(remoteId: string): Promise<RemoteUser|null> { const cached = this.remoteUserCache.get(remoteId); - if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) { - return cached.e; + if (cached) { + MetricPeg.get.storeCall("UserStore.getRemoteUser", true); + return cached; } + MetricPeg.get.storeCall("UserStore.getRemoteUser", false); + const row = await this.db.Get( "SELECT * FROM user_entries WHERE remote_id = $id", {id: remoteId}, ); @@ -81,11 +85,12 @@ export class DbUserStore { remoteUser.guildNicks.set(guild_id as string, nick as string); }); } - this.remoteUserCache.set(remoteId, {e: remoteUser, ts: Date.now()}); + this.remoteUserCache.set(remoteId, remoteUser); return remoteUser; } public async setRemoteUser(user: RemoteUser) { + MetricPeg.get.storeCall("UserStore.setRemoteUser", false); this.remoteUserCache.delete(user.id); const existingData = await this.db.Get( "SELECT * FROM remote_user_data WHERE remote_id = $remoteId", @@ -156,6 +161,7 @@ AND guild_id = $guild_id`, } public async linkUsers(matrixId: string, remoteId: string) { + MetricPeg.get.storeCall("UserStore.linkUsers", false); // This is used ONCE in the bridge to link two IDs, so do not UPSURT data. try { await this.db.Run(`INSERT INTO user_entries VALUES ($matrixId, $remoteId)`, { diff --git a/src/discordas.ts b/src/discordas.ts index 9d72fbae05cf9b85883b73aca571caa022e33694..375f116e895c9ffb4c2d6f233d2e6c9954a7170c 100644 --- a/src/discordas.ts +++ b/src/discordas.ts @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { Appservice, IAppserviceRegistration, ConsoleLogger, LogService } from "matrix-bot-sdk"; +import { Appservice, IAppserviceRegistration, LogService } from "matrix-bot-sdk"; import * as yaml from "js-yaml"; import * as fs from "fs"; import { DiscordBridgeConfig } from "./config"; @@ -25,6 +25,7 @@ import * as cliArgs from "command-line-args"; import * as usage from "command-line-usage"; import * as uuid from "uuid/v4"; import { IMatrixEvent } from "./matrixtypes"; +import { MetricPeg, PrometheusBridgeMetrics } from "./metrics"; const log = new Log("DiscordAS"); @@ -46,17 +47,17 @@ function generateRegistration(opts, registrationPath) { hs_token: uuid(), id: "discord-bridge", namespaces: { - users: [ + aliases: [ { exclusive: true, - regex: "@_discord_.*", + regex: "#_discord_.*", }, ], rooms: [ ], - aliases: [ + users: [ { exclusive: true, - regex: "#_discord_.*", + regex: "@_discord_.*", }, ], }, @@ -68,6 +69,33 @@ function generateRegistration(opts, registrationPath) { fs.writeFileSync(registrationPath, yaml.safeDump(reg)); } +function setupLogging() { + const logMap = new Map<string, Log>(); + const logFunc = (level: string, module: string, args: any[]) => { + if (!Array.isArray(args)) { + args = [args]; + } + if (args.find((s) => s.includes && s.includes("M_USER_IN_USE"))) { + // Spammy logs begon + return; + } + const mod = "bot-sdk" + module; + let logger = logMap.get(mod); + if (!logger) { + logger = new Log(mod); + logMap.set(mod, logger); + } + logger[level](args); + }; + + LogService.setLogger({ + debug: (mod: string, args: any[]) => logFunc("silly", mod, args), + error: (mod: string, args: any[]) => logFunc("error", mod, args), + info: (mod: string, args: any[]) => logFunc("info", mod, args), + warn: (mod: string, args: any[]) => logFunc("warn", mod, args), + }); +} + async function run() { const opts = cliArgs(commandOptions); if (opts.help) { @@ -101,9 +129,10 @@ async function run() { if (!port) { throw Error("Port not given in command line or config file"); } - config.ApplyConfig(yaml.safeLoad(fs.readFileSync(configPath, "utf8"))); + config.applyConfig(yaml.safeLoad(fs.readFileSync(configPath, "utf8"))); Log.Configure(config.logging); const registration = yaml.safeLoad(fs.readFileSync(registrationPath, "utf8")) as IAppserviceRegistration; + setupLogging(); const appservice = new Appservice({ bindAddress: config.bridge.bindAddress || "0.0.0.0", homeserverName: config.bridge.domain, @@ -112,31 +141,6 @@ async function run() { registration, }); - const logMap = new Map<string, Log>(); - const logFunc = (level: string, module: string, args: any[]) => { - if (!Array.isArray(args)) { - args = [args]; - } - if (args.find((s) => s.includes && s.includes("M_USER_IN_USE"))) { - // Spammy logs begon - return; - } - const mod = "bot-sdk" + module; - let logger = logMap.get(mod); - if (!logger) { - logger = new Log(mod); - logMap.set(mod, logger); - } - logger[level](args); - }; - - LogService.setLogger({ - debug: (mod: string, args: any[]) => logFunc("silly", mod, args), - error: (mod: string, args: any[]) => logFunc("error", mod, args), - info: (mod: string, args: any[]) => logFunc("info", mod, args), - warn: (mod: string, args: any[]) => logFunc("warn", mod, args), - }); - const store = new DiscordStore(config.database); if (config.database.roomStorePath) { @@ -149,6 +153,12 @@ async function run() { + "The config option userStorePath no longer has any use."); } + if (config.bridge.enableMetrics) { + log.info("Enabled metrics"); + MetricPeg.set(new PrometheusBridgeMetrics().init()); + } + + try { await store.init(); } catch (ex) { @@ -192,4 +202,4 @@ async function run() { run().catch((err) => { log.error("A fatal error occurred during startup:", err); process.exit(1); -}) \ No newline at end of file +}); \ No newline at end of file diff --git a/src/log.ts b/src/log.ts index c525f7c79842f34934485ba8a809f703db686607..0477d8a1af5e77d3e47289f56a034a6afc3248e3 100644 --- a/src/log.ts +++ b/src/log.ts @@ -17,7 +17,6 @@ limitations under the License. import { createLogger, Logger, format, transports } from "winston"; import { DiscordBridgeConfigLogging, LoggingFile} from "./config"; import { inspect } from "util"; -import * as moment from "moment"; import "winston-daily-rotate-file"; const FORMAT_FUNC = format.printf((info) => { @@ -47,10 +46,6 @@ export class Log { private static config: DiscordBridgeConfigLogging; private static logger: Logger; - private static now() { - return moment().format(Log.config.lineDateFormat); - } - private static setupLogger() { if (Log.logger) { Log.logger.close(); @@ -64,7 +59,7 @@ export class Log { Log.logger = createLogger({ format: format.combine( format.timestamp({ - format: Log.now(), + format: Log.config.lineDateFormat, }), format.colorize(), FORMAT_FUNC, diff --git a/src/matrixeventprocessor.ts b/src/matrixeventprocessor.ts index a65cf805dc08149d3499abb04fafeb22ffb39eb7..4f1c3cb02f32b39c3a47ea36691048c5ff7611b5 100644 --- a/src/matrixeventprocessor.ts +++ b/src/matrixeventprocessor.ts @@ -27,6 +27,8 @@ import { Log } from "./log"; import { IRoomStoreEntry } from "./db/roomstore"; import { Appservice, MatrixClient } from "matrix-bot-sdk"; import { DiscordStore } from "./store"; +import { TimedCache } from "./structures/timedcache"; +import { MetricPeg } from "./metrics"; const log = new Log("MatrixEventProcessor"); @@ -37,6 +39,7 @@ const DISCORD_AVATAR_WIDTH = 128; const DISCORD_AVATAR_HEIGHT = 128; const ROOM_NAME_PARTS = 2; const AGE_LIMIT = 900000; // 15 * 60 * 1000 +const PROFILE_CACHE_LIFETIME = 900000; export class MatrixEventProcessorOpts { constructor( @@ -61,6 +64,7 @@ export class MatrixEventProcessor { private store: DiscordStore; private matrixMsgProcessor: MatrixMessageProcessor; private mxCommandHandler: MatrixCommandHandler; + private mxUserProfileCache: TimedCache<string, {displayname: string, avatar_url: string|undefined}>; constructor(opts: MatrixEventProcessorOpts, cm?: MatrixCommandHandler) { this.config = opts.config; @@ -68,6 +72,7 @@ export class MatrixEventProcessor { this.discord = opts.discord; this.store = opts.store; this.matrixMsgProcessor = new MatrixMessageProcessor(this.discord, this.config.bridge.homeserverUrl); + this.mxUserProfileCache = new TimedCache(PROFILE_CACHE_LIFETIME); if (cm) { this.mxCommandHandler = cm; } else { @@ -79,6 +84,7 @@ export class MatrixEventProcessor { const remoteRoom = rooms[0]; if (event.unsigned.age > AGE_LIMIT) { log.warn(`Skipping event due to age ${event.unsigned.age} > ${AGE_LIMIT}`); + MetricPeg.get.requestOutcome(event.event_id, false, "dropped"); return; } if ( @@ -130,12 +136,11 @@ export class MatrixEventProcessor { const srvChanPair = remoteRoom.remote!.roomId.substr("_discord".length).split("_", ROOM_NAME_PARTS); try { await this.ProcessMsgEvent(event, srvChanPair[0], srvChanPair[1]); - return; } catch (err) { log.warn("There was an error sending a matrix event", err); - return; } } + return; } else if (event.type === "m.room.encryption" && remoteRoom) { try { await this.HandleEncryptionWarning(event.room_id); @@ -143,10 +148,9 @@ export class MatrixEventProcessor { } catch (err) { throw new Error(`Failed to handle encrypted room, ${err}`); } - } else { - log.verbose("Got non m.room.message event"); } log.verbose("Event not processed by bridge"); + MetricPeg.get.requestOutcome(event.event_id, false, "dropped"); } public async HandleEncryptionWarning(roomId: string): Promise<void> { @@ -172,7 +176,6 @@ export class MatrixEventProcessor { log.verbose(`Looking up ${guildId}_${channelId}`); const roomLookup = await this.discord.LookupRoom(guildId, channelId, event.sender); const chan = roomLookup.channel; - const botUser = roomLookup.botUser; const embedSet = await this.EventToEmbed(event, chan); const opts: Discord.MessageOptions = {}; @@ -184,7 +187,10 @@ export class MatrixEventProcessor { } await this.discord.send(embedSet, opts, roomLookup, event); - await this.sendReadReceipt(event); + // Don't await this. + this.sendReadReceipt(event).catch((ex) => { + log.verbose("Failed to send read reciept for ", event.event_id, ex); + }); } public async ProcessStateEvent(event: IMatrixEvent) { @@ -204,31 +210,42 @@ export class MatrixEventProcessor { let msg = `\`${event.sender}\` `; + const allowJoinLeave = !this.config.bridge.disableJoinLeaveNotifications; + if (event.type === "m.room.name") { msg += `set the name to \`${event.content!.name}\``; } else if (event.type === "m.room.topic") { msg += `set the topic to \`${event.content!.topic}\``; } else if (event.type === "m.room.member") { const membership = event.content!.membership; - if (membership === "join" - && event.unsigned.prev_content === undefined) { - if (!this.config.bridge.disableJoinLeaveNotifications) { - msg += `joined the room`; - } else { - return; - } + const client = this.bridge.botIntent.underlyingClient; + const isNewJoin = event.unsigned.replaces_state === undefined ? true : ( + await client.getEvent(event.room_id, event.unsigned.replaces_state)).content.membership !== "join"; + if (membership === "join") { + this.mxUserProfileCache.delete(`${event.room_id}:${event.sender}`); + this.mxUserProfileCache.delete(event.sender); + if (event.content!.displayname) { + this.mxUserProfileCache.set(`${event.room_id}:${event.sender}`, { + avatar_url: event.content!.avatar_url, + displayname: event.content!.displayname!, + }); + } + // We don't know if the user also updated their profile, but to be safe.. + this.mxUserProfileCache.delete(event.sender); + } + if (membership === "join" && isNewJoin && allowJoinLeave) { + msg += "joined the room"; } else if (membership === "invite") { msg += `invited \`${event.state_key}\` to the room`; } else if (membership === "leave" && event.state_key !== event.sender) { msg += `kicked \`${event.state_key}\` from the room`; - } else if (membership === "leave") { - if (!this.config.bridge.disableJoinLeaveNotifications) { - msg += `left the room`; - } else { - return; - } + } else if (membership === "leave" && allowJoinLeave) { + msg += "left the room"; } else if (membership === "ban") { msg += `banned \`${event.state_key}\` from the room`; + } else { + // Ignore anything else + return; } } @@ -241,19 +258,7 @@ export class MatrixEventProcessor { event: IMatrixEvent, channel: Discord.TextChannel, getReply: boolean = true, ): Promise<IMatrixEventProcessorResult> { const mxClient = this.bridge.botIntent.underlyingClient; - let profile: IMatrixEvent | null = null; - try { - profile = await mxClient.getRoomStateEvent(event.room_id, "m.room.member", event.sender); - if (!profile) { - profile = await mxClient.getUserProfile(event.sender); - } - if (!profile) { - log.warn(`User ${event.sender} has no member state and no profile. That's odd.`); - } - } catch (err) { - log.warn(`Trying to fetch member state or profile for ${event.sender} failed`, err); - } - + const profile = await this.GetUserProfileForRoom(event.room_id, event.sender); const params = { mxClient, roomId: event.room_id, @@ -377,6 +382,44 @@ export class MatrixEventProcessor { return embed; } + private async GetUserProfileForRoom(roomId: string, userId: string) { + const mxClient = this.bridge.botIntent.underlyingClient; + let profile: {displayname: string, avatar_url: string|undefined} | undefined; + try { + // First try to pull out the room-specific profile from the cache. + profile = this.mxUserProfileCache.get(`${roomId}:${userId}`); + if (profile) { + return profile; + } + log.verbose(`Profile ${userId}:${roomId} not cached`); + + // Failing that, try fetching the state. + profile = await mxClient.getRoomStateEvent(roomId, "m.room.member", userId); + if (profile) { + this.mxUserProfileCache.set(`${roomId}:${userId}`, profile); + return profile; + } + + // Try fetching the users profile from the cache + profile = this.mxUserProfileCache.get(userId); + if (profile) { + return profile; + } + + // Failing that, try fetching the profile. + log.verbose(`Profile ${userId} not cached`); + profile = await mxClient.getUserProfile(userId); + if (profile) { + this.mxUserProfileCache.set(userId, profile); + return profile; + } + log.warn(`User ${userId} has no member state and no profile. That's odd.`); + } catch (err) { + log.warn(`Trying to fetch member state or profile for ${userId} failed`, err); + } + return undefined; + } + private async sendReadReceipt(event: IMatrixEvent) { if (!this.config.bridge.disableReadReceipts) { try { @@ -404,8 +447,9 @@ export class MatrixEventProcessor { return hasAttachment; } - private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: IMatrixEvent | null) { - const intent = this.bridge.botIntent; + private async SetEmbedAuthor(embed: Discord.RichEmbed, sender: string, profile?: { + displayname: string, + avatar_url: string|undefined }) { let displayName = sender; let avatarUrl; @@ -428,13 +472,6 @@ export class MatrixEventProcessor { } // Let it fall through. } - if (!profile) { - try { - profile = await intent.underlyingClient.getUserProfile(sender); - } catch (ex) { - log.warn(`Failed to fetch profile for ${sender}`, ex); - } - } if (profile) { if (profile.displayname && diff --git a/src/matrixtypes.ts b/src/matrixtypes.ts index 7535b59a89063a02e1f1946321550287f483dd94..f08ae1301c571fdbc866e7aafff94eed21e8229f 100644 --- a/src/matrixtypes.ts +++ b/src/matrixtypes.ts @@ -23,6 +23,7 @@ export interface IMatrixEventContent { msgtype?: string; url?: string; displayname?: string; + avatar_url?: string; reason?: string; "m.relates_to"?: any; // tslint:disable-line no-any } diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e228a11f64b1d0f2d38e755615552399f413a0f --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,155 @@ +/* +Copyright 2019 matrix-appservice-discord + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { Gauge, Counter, Histogram } from "prom-client"; +import { Log } from "./log"; +import { Appservice } from "matrix-bot-sdk"; + +const AgeCounters = PrometheusMetrics.AgeCounters; +const log = new Log("BridgeMetrics"); +const REQUEST_EXPIRE_TIME_MS = 30000; + +interface IAgeCounter { + setGauge(gauge: Gauge, morelabels: string[]); + bump(age: number); +} + +interface IBridgeGauges { + matrixRoomConfigs: number; + remoteRoomConfigs: number; + matrixGhosts: number; + remoteGhosts: number; + matrixRoomsByAge: IAgeCounter; + remoteRoomsByAge: IAgeCounter; + matrixUsersByAge: IAgeCounter; + remoteUsersByAge: IAgeCounter; +} + +export interface IBridgeMetrics { + registerRequest(id: string); + requestOutcome(id: string, isRemote: boolean, outcome: string); + remoteCall(method: string); + setPresenceCount(count: number); + storeCall(method: string, cached: boolean); +} + +export class DummyBridgeMetrics implements IBridgeMetrics { + public registerRequest() {} + public requestOutcome() {} + public remoteCall() {} + public setPresenceCount() {} + public storeCall() {} +} + +export class MetricPeg { + public static get get(): IBridgeMetrics { + return this.metrics; + } + + public static set(metrics: IBridgeMetrics) { + this.metrics = metrics; + } + + private static metrics: IBridgeMetrics = new DummyBridgeMetrics(); +} + +export class PrometheusBridgeMetrics implements IBridgeMetrics { + private metrics; + private remoteCallCounter: Counter; + private storeCallCounter: Counter; + private presenceGauge: Gauge; + private remoteRequest: Histogram; + private matrixRequest: Histogram; + private requestsInFlight: Map<string, number>; + private bridgeGauges: IBridgeGauges = { + matrixGhosts: 0, + matrixRoomConfigs: 0, + matrixRoomsByAge: new AgeCounters(), + matrixUsersByAge: new AgeCounters(), + remoteGhosts: 0, + remoteRoomConfigs: 0, + remoteRoomsByAge: new AgeCounters(), + remoteUsersByAge: new AgeCounters(), + }; + + public init(as: Appservice) { + this.metrics = new PrometheusMetrics(); + this.metrics.registerMatrixSdkMetrics(); + this.metrics.registerBridgeGauges(() => this.bridgeGauges); + this.metrics.addAppServicePath(bridge); + this.remoteCallCounter = this.metrics.addCounter({ + help: "Count of remote API calls made", + labels: ["method"], + name: "remote_api_calls", + }); + this.storeCallCounter = this.metrics.addCounter({ + help: "Count of store function calls made", + labels: ["method", "cached"], + name: "store_calls", + }); + this.presenceGauge = this.metrics.addGauge({ + help: "Count of users in the presence queue", + labels: [], + + name: "active_presence_users", + }); + this.matrixRequest = this.metrics.addTimer({ + help: "Histogram of processing durations of received Matrix messages", + labels: ["outcome"], + name: "matrix_request_seconds", + }); + this.remoteRequest = this.metrics.addTimer({ + help: "Histogram of processing durations of received remote messages", + labels: ["outcome"], + name: "remote_request_seconds", + }); + this.requestsInFlight = new Map(); + setInterval(() => { + this.requestsInFlight.forEach((time, id) => { + if (Date.now() - time) { + this.requestsInFlight.delete(id); + } + }); + }, REQUEST_EXPIRE_TIME_MS); + return this; + } + + public registerRequest(id: string) { + this.requestsInFlight.set(id, Date.now()); + } + + public requestOutcome(id: string, isRemote: boolean, outcome: string) { + const startTime = this.requestsInFlight.get(id); + if (!startTime) { + return; + } + this.requestsInFlight.delete(id); + const duration = Date.now() - startTime; + (isRemote ? this.remoteRequest : this.matrixRequest).observe({outcome}, duration / 1000); + } + + public setPresenceCount(count: number) { + this.presenceGauge.set(count); + } + + public remoteCall(method: string) { + this.remoteCallCounter.inc({method}); + } + + public storeCall(method: string, cached: boolean) { + this.storeCallCounter.inc({method, cached: cached ? "yes" : "no"}); + } +} diff --git a/src/presencehandler.ts b/src/presencehandler.ts index d4d0fc364c8ca36b5cd0bdce528eae0397abe663..9e064ab33142e60925f342ca249f60ea4ee23918 100644 --- a/src/presencehandler.ts +++ b/src/presencehandler.ts @@ -17,6 +17,7 @@ limitations under the License. import { User, Presence } from "discord.js"; import { DiscordBot } from "./bot"; import { Log } from "./log"; +import { MetricPeg } from "./metrics"; const log = new Log("PresenceHandler"); export class PresenceHandlerStatus { @@ -65,6 +66,7 @@ export class PresenceHandler { if (user.id !== this.bot.GetBotId() && this.presenceQueue.find((u) => u.id === user.id) === undefined) { log.verbose(`Adding ${user.id} (${user.username}) to the presence queue`); this.presenceQueue.push(user); + MetricPeg.get.setPresenceCount(this.presenceQueue.length); } } @@ -74,6 +76,7 @@ export class PresenceHandler { }); if (index !== -1) { this.presenceQueue.splice(index, 1); + MetricPeg.get.setPresenceCount(this.presenceQueue.length); } else { log.warn( `Tried to remove ${user.id} from the presence queue but it could not be found`, @@ -95,6 +98,7 @@ export class PresenceHandler { this.presenceQueue.push(user); } else { log.verbose(`Dropping ${user.id} from the presence queue.`); + MetricPeg.get.setPresenceCount(this.presenceQueue.length); } } } diff --git a/src/store.ts b/src/store.ts index 7793e8190c78f7ec76cfb2446f27c2094cb5de8e..ddb35cd2d9cc2e87e3855671fbf4305330cb809b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -68,7 +68,7 @@ export class DiscordStore { return resolve(err === null); }); }).then(async (result) => { - return new Promise((resolve, reject) => { + return new Promise<void|{}>((resolve, reject) => { if (!result) { log.warn("NOT backing up database while a file already exists"); resolve(true); @@ -226,7 +226,6 @@ export class DiscordStore { throw err; } } - // tslint:disable-next-line no-any public async Get<T extends IDbData>(dbType: {new(): T; }, params: any): Promise<T|null> { const dType = new dbType(); diff --git a/src/structures/timedcache.ts b/src/structures/timedcache.ts new file mode 100644 index 0000000000000000000000000000000000000000..93424af9d488d493439bd4294dc969297f79eeee --- /dev/null +++ b/src/structures/timedcache.ts @@ -0,0 +1,106 @@ +interface ITimedValue<V> { + value: V; + ts: number; +} + +export class TimedCache<K, V> implements Map<K, V> { + private readonly map: Map<K, ITimedValue<V>>; + + public constructor(private readonly liveFor: number) { + this.map = new Map(); + } + + public clear(): void { + this.map.clear(); + } + + public delete(key: K): boolean { + return this.map.delete(key); + } + + public forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void|Promise<void>): void { + for (const item of this) { + callbackfn(item[1], item[0], this); + } + } + + public get(key: K): V | undefined { + const v = this.map.get(key); + if (v === undefined) { + return; + } + const val = this.filterV(v); + if (val !== undefined) { + return val; + } + // Cleanup expired key + this.map.delete(key); + } + + public has(key: K): boolean { + return this.get(key) !== undefined; + } + + public set(key: K, value: V): this { + this.map.set(key, { + ts: Date.now(), + value, + }); + return this; + } + + public get size(): number { + return this.map.size; + } + + public [Symbol.iterator](): IterableIterator<[K, V]> { + let iterator: IterableIterator<[K, ITimedValue<V>]>; + return { + next: () => { + if (!iterator) { + iterator = this.map.entries(); + } + let item: IteratorResult<[K, ITimedValue<V>]>|undefined; + let filteredValue: V|undefined; + // Loop if we have no item, or the item has expired. + while (!item || filteredValue === undefined) { + item = iterator.next(); + // No more items in map. Bye bye. + if (item.done) { + break; + } + filteredValue = this.filterV(item.value[1]); + } + if (item.done) { + // Typscript doesn't like us returning undefined for value, which is dumb. + // tslint:disable-next-line: no-any + return {done: true, value: undefined} as any as IteratorResult<[K, V]>; + } + return {done: false, value: [item.value[0], filteredValue]} as IteratorResult<[K, V]>; + }, + [Symbol.iterator]: () => this[Symbol.iterator](), + }; + } + + public entries(): IterableIterator<[K, V]> { + return this[Symbol.iterator](); + } + + public keys(): IterableIterator<K> { + throw new Error("Method not implemented."); + } + + public values(): IterableIterator<V> { + throw new Error("Method not implemented."); + } + + get [Symbol.toStringTag](): "Map" { + return "Map"; + } + + private filterV(v: ITimedValue<V>): V|undefined { + if (Date.now() - v.ts < this.liveFor) { + return v.value; + } + } +} diff --git a/test/config.ts b/test/config.ts index b6d7f447d8cf09471281abc3d3c84107f5fe1b2b..13b2ea444cf91e349167587d68f836afcf5252c1 100644 --- a/test/config.ts +++ b/test/config.ts @@ -18,11 +18,6 @@ import { argv } from "process"; import { Log } from "../src/log"; import * as WhyRunning from "why-is-node-running"; -const logger = new Log("MessageProcessor"); - -// we are a test file and thus need those -/* tslint:disable:no-unused-expression max-file-line-count */ - if (!argv.includes("--noisy")) { Log.ForceSilent(); } diff --git a/test/mocks/discordclientfactory.ts b/test/mocks/discordclientfactory.ts index 803dedc118f7cacbb9c44443840dee85c06c4c4b..b174f3d5f9455581b13c2a3460f4eee4a32caa7d 100644 --- a/test/mocks/discordclientfactory.ts +++ b/test/mocks/discordclientfactory.ts @@ -31,4 +31,6 @@ export class DiscordClientFactory { } return this.botClient; } + + public bindMetricsToChannel() {} } diff --git a/test/structures/test_timedcache.ts b/test/structures/test_timedcache.ts new file mode 100644 index 0000000000000000000000000000000000000000..f002fc77ee182cb14f839c52a39f3228ce02ef67 --- /dev/null +++ b/test/structures/test_timedcache.ts @@ -0,0 +1,124 @@ +/* +Copyright 2019 matrix-appservice-discord + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { expect } from "chai"; +import { TimedCache } from "../../src/structures/timedcache"; +import { Util } from "../../src/util"; + +// we are a test file and thus need those +/* tslint:disable:no-unused-expression max-file-line-count no-any */ + +describe("TimedCache", () => { + it("should construct", () => { + const timedCache = new TimedCache<string, number>(1000); + expect(timedCache.size).to.equal(0); + }); + + it("should add and get values", () => { + const timedCache = new TimedCache<string, number>(1000); + timedCache.set("foo", 1); + timedCache.set("bar", -1); + timedCache.set("baz", 0); + expect(timedCache.get("foo")).to.equal(1); + expect(timedCache.get("bar")).to.equal(-1); + expect(timedCache.get("baz")).to.equal(0); + }); + + it("should be able to overwrite values", () => { + const timedCache = new TimedCache<string, number>(1000); + timedCache.set("foo", 1); + expect(timedCache.get("foo")).to.equal(1); + timedCache.set("bar", 0); + timedCache.set("foo", -1); + expect(timedCache.get("bar")).to.equal(0); + expect(timedCache.get("foo")).to.equal(-1); + }); + + it("should be able to check if a value exists", () => { + const timedCache = new TimedCache<string, number>(1000); + expect(timedCache.has("foo")).to.be.false; + timedCache.set("foo", 1); + expect(timedCache.has("foo")).to.be.true; + timedCache.set("bar", 1); + expect(timedCache.has("bar")).to.be.true; + }); + + it("should be able to delete a value", () => { + const timedCache = new TimedCache<string, number>(1000); + timedCache.set("foo", 1); + expect(timedCache.has("foo")).to.be.true; + timedCache.delete("foo"); + expect(timedCache.has("foo")).to.be.false; + expect(timedCache.get("foo")).to.be.undefined; + }); + + it("should expire a value", async () => { + const LIVE_FOR = 50; + const timedCache = new TimedCache<string, number>(LIVE_FOR); + timedCache.set("foo", 1); + expect(timedCache.has("foo")).to.be.true; + expect(timedCache.get("foo")).to.equal(1); + await Util.DelayedPromise(LIVE_FOR); + expect(timedCache.has("foo")).to.be.false; + expect(timedCache.get("foo")).to.be.undefined; + }); + + it("should be able to iterate around a long-lasting collection", () => { + const timedCache = new TimedCache<string, number>(1000); + timedCache.set("foo", 1); + timedCache.set("bar", -1); + timedCache.set("baz", 0); + let i = 0; + for (const iterator of timedCache) { + if (i === 0) { + expect(iterator[0]).to.equal("foo"); + expect(iterator[1]).to.equal(1); + } else if (i === 1) { + expect(iterator[0]).to.equal("bar"); + expect(iterator[1]).to.equal(-1); + } else { + expect(iterator[0]).to.equal("baz"); + expect(iterator[1]).to.equal(0); + } + i++; + } + }); + + it("should be able to iterate around a short-term collection", async () => { + const LIVE_FOR = 100; + const timedCache = new TimedCache<string, number>(LIVE_FOR); + timedCache.set("foo", 1); + timedCache.set("bar", -1); + timedCache.set("baz", 0); + let i = 0; + for (const iterator of timedCache) { + if (i === 0) { + expect(iterator[0]).to.equal("foo"); + expect(iterator[1]).to.equal(1); + } else if (i === 1) { + expect(iterator[0]).to.equal("bar"); + expect(iterator[1]).to.equal(-1); + } else { + expect(iterator[0]).to.equal("baz"); + expect(iterator[1]).to.equal(0); + } + i++; + } + await Util.DelayedPromise(LIVE_FOR); + const vals = [...timedCache.entries()]; + expect(vals).to.be.empty; + }); +}); diff --git a/test/test_channelsyncroniser.ts b/test/test_channelsyncroniser.ts index 67fdeb311a375e5bf9a5b2bd2929cdcb7ecca21f..50308d9dad89ee8e7a0be3a30c0395cb4039e562 100644 --- a/test/test_channelsyncroniser.ts +++ b/test/test_channelsyncroniser.ts @@ -387,6 +387,33 @@ describe("ChannelSyncroniser", () => { expect(state.mxChannels[0].iconUrl).equals("https://cdn.discordapp.com/icons/654321/new_icon.png"); expect(state.mxChannels[0].iconId).equals("new_icon"); }); + it("will update animated icons", async () => { + const guild = new MockGuild("654321", [], "newGuild"); + guild.icon = "a_new_icon"; + const chan = new MockChannel(); + chan.type = "text"; + chan.id = "blah"; + chan.guild = guild; + + const testStore = [ + new Entry({ + id: "1", + matrix_id: "!1:localhost", + remote: { + discord_channel: chan.id, + discord_iconurl: "https://cdn.discordapp.com/icons/654321/old_icon.png", + update_icon: true, + }, + remote_id: "111", + }), + ]; + + const channelSync = CreateChannelSync(testStore); + const state = await channelSync.GetChannelUpdateState(chan as any); + expect(state.mxChannels.length).equals(1); + expect(state.mxChannels[0].iconUrl).equals("https://cdn.discordapp.com/icons/654321/a_new_icon.gif"); + expect(state.mxChannels[0].iconId).equals("a_new_icon"); + }); it("won't update the icon", async () => { const guild = new MockGuild("654321", [], "newGuild"); guild.icon = "new_icon"; diff --git a/test/test_config.ts b/test/test_config.ts index 0979897b55ea157ff94e02386674097ab0516645..badb662a5c4a9929bfdfacb22c45faeef09a965d 100644 --- a/test/test_config.ts +++ b/test/test_config.ts @@ -22,10 +22,10 @@ import { DiscordBridgeConfig } from "../src/config"; const expect = Chai.expect; -describe("DiscordBridgeConfig.ApplyConfig", () => { +describe("DiscordBridgeConfig.applyConfig", () => { it("should merge configs correctly", () => { const config = new DiscordBridgeConfig(); - config.ApplyConfig({ + config.applyConfig({ bridge: { disableDeletionForwarding: true, disableDiscordMentions: false, @@ -38,17 +38,40 @@ describe("DiscordBridgeConfig.ApplyConfig", () => { console: "warn", }, }); - expect(config.bridge.homeserverUrl, "blah"); + expect(config.bridge.homeserverUrl).to.equal("blah"); expect(config.bridge.disableTypingNotifications).to.be.true; expect(config.bridge.disableDiscordMentions).to.be.false; expect(config.bridge.disableDeletionForwarding).to.be.true; expect(config.bridge.enableSelfServiceBridging).to.be.false; expect(config.bridge.disableJoinLeaveNotifications).to.be.true; - expect(config.logging.console, "warn"); + expect(config.logging.console).to.equal("warn"); + }); + it("should merge environment overrides correctly", () => { + const config = new DiscordBridgeConfig(); + config.applyConfig({ + bridge: { + disableDeletionForwarding: true, + disableDiscordMentions: false, + homeserverUrl: "blah", + }, + logging: { + console: "warn", + }, + }); + config.applyEnvironmentOverrides({ + APPSERVICE_DISCORD_BRIDGE_DISABLE_DELETION_FORWARDING: false, + APPSERVICE_DISCORD_BRIDGE_DISABLE_JOIN_LEAVE_NOTIFICATIONS: true, + APPSERVICE_DISCORD_LOGGING_CONSOLE: "debug", + }); + expect(config.bridge.disableJoinLeaveNotifications).to.be.true; + expect(config.bridge.disableDeletionForwarding).to.be.false; + expect(config.bridge.disableDiscordMentions).to.be.false; + expect(config.bridge.homeserverUrl).to.equal("blah"); + expect(config.logging.console).to.equal("debug"); }); it("should merge logging.files correctly", () => { const config = new DiscordBridgeConfig(); - config.ApplyConfig({ + config.applyConfig({ logging: { console: "silent", files: [ @@ -58,6 +81,6 @@ describe("DiscordBridgeConfig.ApplyConfig", () => { ], }, }); - expect(config.logging.files[0].file, "./bacon.log"); + expect(config.logging.files[0].file).to.equal("./bacon.log"); }); }); diff --git a/test/test_discordbot.ts b/test/test_discordbot.ts index b4a398b238f96cd243aa03d4d9749207b93eff2e..10df5fd801e4b366a2d6b818e33787af382daa7b 100644 --- a/test/test_discordbot.ts +++ b/test/test_discordbot.ts @@ -25,6 +25,8 @@ import { MockMessage } from "./mocks/message"; import { Util } from "../src/util"; import { AppserviceMock } from "./mocks/appservicemock"; import { MockUser } from "./mocks/user"; +import { MockChannel } from "./mocks/channel"; +import { DiscordBot } from "../src/bot"; // we are a test file and thus need those /* tslint:disable:no-unused-expression max-file-line-count no-any */ @@ -43,11 +45,11 @@ const modDiscordBot = Proxyquire("../src/bot", { AsyncForEach: Util.AsyncForEach, DelayedPromise: Util.DelayedPromise, DownloadFile: async () => { - return Buffer.alloc(1024); + return Buffer.alloc(1000); }, UploadContentFromUrl: async () => { return {mxcUrl: "uploaded"}; - } + }, }, }, }); @@ -181,10 +183,10 @@ describe("DiscordBot", () => { body: "someimage.png", external_url: "asdf", info: { + h: 0, mimetype: "image/png", size: 42, w: 0, - h: 0, }, msgtype: "m.image", url: "mxc://someimage.png", @@ -206,10 +208,10 @@ describe("DiscordBot", () => { body: "foxes.mov", external_url: "asdf", info: { + h: 0, mimetype: "video/quicktime", size: 42, w: 0, - h: 0, }, msgtype: "m.video", url: "mxc://foxes.mov", @@ -255,7 +257,7 @@ describe("DiscordBot", () => { external_url: "asdf", info: { mimetype: "application/zip", - size: 42 + size: 42, }, msgtype: "m.file", url: "mxc://meow.zip", @@ -401,4 +403,46 @@ describe("DiscordBot", () => { assert.equal(expected, ITERATIONS); }); }); + describe("locks", () => { + it("should lock and unlock a channel", async () => { + const bot = new modDiscordBot.DiscordBot( + "", + config, + mockBridge, + {}, + ) as DiscordBot; + const chan = new MockChannel("123") as any; + const t = Date.now(); + bot.lockChannel(chan); + await bot.waitUnlock(chan); + const diff = Date.now() - t; + expect(diff).to.be.greaterThan(config.limits.discordSendDelay - 1); + }); + it("should lock and unlock a channel early, if unlocked", async () => { + const discordSendDelay = 500; + const SHORTDELAY = 100; + const MINEXPECTEDDELAY = 95; + const bot = new modDiscordBot.DiscordBot( + "", + { + bridge: { + domain: "localhost", + }, + limits: { + discordSendDelay, + }, + }, + mockBridge, + {}, + ) as DiscordBot; + const chan = new MockChannel("123") as any; + setTimeout(() => bot.unlockChannel(chan), SHORTDELAY); + const t = Date.now(); + bot.lockChannel(chan); + await bot.waitUnlock(chan); + const diff = Date.now() - t; + // Date accuracy can be off by a few ms sometimes. + expect(diff).to.be.greaterThan(MINEXPECTEDDELAY); + }); + }); }); diff --git a/test/test_discordmessageprocessor.ts b/test/test_discordmessageprocessor.ts index f78d66abded7dacaa4451b546796459cf7776e64..d66dd949e2cbe76da2780a1853e5acd6775ee4b6 100644 --- a/test/test_discordmessageprocessor.ts +++ b/test/test_discordmessageprocessor.ts @@ -58,8 +58,8 @@ describe("DiscordMessageProcessor", () => { msg.embeds = []; msg.content = "Hello World!"; const result = await processor.FormatMessage(msg); - Chai.assert(result.body, "Hello World!"); - Chai.assert(result.formattedBody, "Hello World!"); + Chai.assert.equal(result.body, "Hello World!"); + Chai.assert.equal(result.formattedBody, "Hello World!"); }); it("processes markdown messages correctly.", async () => { const processor = new DiscordMessageProcessor( diff --git a/test/test_matrixeventprocessor.ts b/test/test_matrixeventprocessor.ts index 4e9c8a7605303f8d651096e6689f398ebe3b1d78..03ba47112152cd1d654e5b66e2d071893d10d81f 100644 --- a/test/test_matrixeventprocessor.ts +++ b/test/test_matrixeventprocessor.ts @@ -636,6 +636,7 @@ describe("MatrixEventProcessor", () => { }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -657,6 +658,7 @@ This is where the reply goes`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -678,6 +680,7 @@ This is where the reply goes`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -699,6 +702,7 @@ This is the second reply`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -720,6 +724,7 @@ This is the reply`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -739,10 +744,12 @@ This is the reply`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); - expect(result!.timestamp!.getTime()).to.be.equal(TEST_TIMESTAMP); + // NOTE: Due to https://github.com/discordjs/discord.js/issues/3283, the typing is wrong here. + expect(result!.timestamp!).to.be.equal(TEST_TIMESTAMP); }); it("should add field for discord replies", async () => { const processor = createMatrixEventProcessor(); @@ -755,6 +762,7 @@ This is the reply`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -779,6 +787,7 @@ This is the reply`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); @@ -796,6 +805,7 @@ This is the reply`, }, }, }, + room_id: "!fakeroom:localhost", sender: "@test:localhost", type: "m.room.message", } as IMatrixEvent, mockChannel as any); diff --git a/tools/toolshelper.ts b/tools/toolshelper.ts index 1d55842de19d4cb9d492d87152e49e7a40f18866..5e4dd35e62bb6b9d49ef34bbac58558c49894808 100644 --- a/tools/toolshelper.ts +++ b/tools/toolshelper.ts @@ -13,7 +13,7 @@ export class ToolsHelper { } { const registration = yaml.safeLoad(fs.readFileSync(regFile, "utf8")); const config: DiscordBridgeConfig = yaml.safeLoad(fs.readFileSync(configFile, "utf8")) as DiscordBridgeConfig; - + config.applyEnvironmentOverrides(process.env); if (registration === null) { throw Error("Failed to parse registration file"); } @@ -33,4 +33,4 @@ export class ToolsHelper { store, }; } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index b56c806b3b13595d99985097b41625102bdbf7fe..0513a4541aea9a8adcccf2bae90bd62895acad66 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "inlineSourceMap": true, "outDir": "./build", "types": ["mocha", "node"], - "strictNullChecks": true + "strictNullChecks": true, + "allowSyntheticDefaultImports": true }, "compileOnSave": true, "include": [ diff --git a/tslint.json b/tslint.json index cd7de561c3b3ba02450df7cb5bf011afd9c73be2..ff80b039be7c4830651871937b672e77c31faca3 100644 --- a/tslint.json +++ b/tslint.json @@ -9,7 +9,7 @@ "object-literal-sort-keys": "off", "no-any": true, "arrow-return-shorthand": true, - "no-magic-numbers": true, + "no-magic-numbers": [true, -1, 0, 1, 1000], "prefer-for-of": true, "typedef": { "severity": "warning"