diff --git a/.env.defaults b/.env.defaults index 3a24b62..fa5c957 100644 --- a/.env.defaults +++ b/.env.defaults @@ -17,10 +17,10 @@ HUBS_HOSTS=localhost,hubs.local # bridge chat from Hubs through to Discord. HUBS_HOOK=Hubs -# The shard ID for this client. (0 if you aren't using multiple shards.) +# The shard ID for this client. (1 if you aren't using multiple shards.) SHARD_ID=0 -# The shard count for this client. (0 if you aren't using multiple shards.) +# The shard count for this client. (1 if you aren't using multiple shards.) SHARD_COUNT=0 # The BCP 47 locale for bot output. diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1234e4c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Local via NPM", + "request": "launch", + "runtimeArgs": [ + "run-script", + "local" + ], + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + }, + { + "name": "Launch via NPM", + "request": "launch", + "runtimeArgs": [ + "run-script", + "start" + ], + "runtimeExecutable": "npm", + "skipFiles": [ + "/**" + ], + "type": "pwa-node" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..78ecd9b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + // Format on save for Prettier + "editor.formatOnSave": true, + // Disable html formatting for now + "html.format.enable": false, + // Disable the default javascript formatter + "javascript.format.enable": false, + // Use 'prettier-eslint' instead of 'prettier'. Other settings will only be fallbacks in case they could not be inferred from eslint rules. + "prettier.eslintIntegration": true +} \ No newline at end of file diff --git a/habitat/plan.sh b/habitat/plan.sh index 2821c7b..13707df 100644 --- a/habitat/plan.sh +++ b/habitat/plan.sh @@ -1,7 +1,7 @@ pkg_name=hubs-discord-bot pkg_origin=mozillareality pkg_maintainer="Mozilla Mixed Reality " -pkg_version="0.0.1" +pkg_version="0.0.2" pkg_license=('MPL2') pkg_description="Discord bot for Hubs by Mozilla" diff --git a/package-lock.json b/package-lock.json index 4cf37d3..0181995 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hubs-discord-bot", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -24,6 +24,21 @@ "js-tokens": "^4.0.0" } }, + "@discordjs/collection": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", + "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" + }, + "@discordjs/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "@sentry/core": { "version": "5.23.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.23.0.tgz", @@ -98,6 +113,14 @@ "tslib": "^1.9.3" } }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "acorn": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", @@ -174,6 +197,11 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -267,6 +295,14 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "combined-stream": { + "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" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -342,16 +378,31 @@ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "discord.js": { - "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": "^6.0.0" + "version": "12.4.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.4.1.tgz", + "integrity": "sha512-KxOB8LOAN3GmrvkD6a6Fr1nlfArIFZ+q7Uqg4T/5duB90GZy9a0/Py2E+Y+eHKP6ZUCR2mbNMLCcHGjahiaNqA==", + "requires": { + "@discordjs/collection": "^0.1.6", + "@discordjs/form-data": "^3.0.1", + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.1", + "prism-media": "^1.2.2", + "setimmediate": "^1.0.5", + "tweetnacl": "^1.0.3", + "ws": "^7.3.1" + }, + "dependencies": { + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + } } }, "doctrine": { @@ -375,7 +426,7 @@ "dev": true }, "erlpack": { - "version": "github:hammerandchisel/erlpack#5d0064f9e106841e1eead711a6451f99b0d289fd", + "version": "github:hammerandchisel/erlpack#e27db8f82892bdb9b28a0547cc394d68b5d2242d", "from": "github:hammerandchisel/erlpack", "requires": { "bindings": "^1.5.0", @@ -587,6 +638,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -936,11 +992,6 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -951,6 +1002,19 @@ "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=" }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -1027,6 +1091,11 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.6.3.tgz", "integrity": "sha512-FXWH6mqjWgU8ewuahp4spec8LkroFZK2NicOv6bNwZC3kcwZUI8LeZdG80UzTSLLhK4T7MsgNwlYDVRlDdfTDg==" }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, "node-gyp-build": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz", @@ -1130,9 +1199,9 @@ "dev": true }, "prism-media": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-0.0.3.tgz", - "integrity": "sha512-c9KkNifSMU/iXT8FFTaBwBMr+rdVcN+H/uNv1o+CuFeTThNZNTOrQ+RgXA1yL/DeLk098duAeRPP3QNPNbhxYQ==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.2.tgz", + "integrity": "sha512-I+nkWY212lJ500jLe4tN9tWO7nRiBAVdMv76P9kffZjYhw20raMlW1HSSvS+MLXC9MmbNZCazMrAr+5jEEgTuw==" }, "progress": { "version": "2.0.3", @@ -1225,6 +1294,11 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -1265,11 +1339,6 @@ } } }, - "snekfetch": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/snekfetch/-/snekfetch-3.6.4.tgz", - "integrity": "sha512-NjxjITIj04Ffqid5lqr7XdgwM7X61c/Dns073Ly170bPQHLm6jkmelye/eglS++1nfTWktpP6Y2bFXjdPlQqdw==" - }, "sorted-array-functions": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz", @@ -1468,9 +1537,9 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tweetnacl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.1.tgz", - "integrity": "sha512-kcoMoKTPYnoeS50tzoqjPY3Uv9axeuuFAZY9M/9zFnhoVvRfxz9K29IMPD7jGmt2c8SW7i3gT9WqDl2+nV7p4A==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, "type-check": { "version": "0.3.2", @@ -1550,4 +1619,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index c38eff6..a2b6261 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hubs-discord-bot", - "version": "0.0.1", + "version": "0.0.2", "description": "A Discord bot that does helpful things related to Hubs rooms.", "repository": "github:MozillaReality/hubs-discord-bot", "main": "src/index.js", @@ -21,7 +21,7 @@ "dependencies": { "@sentry/node": "^5.23.0", "bufferutil": "^4.0.1", - "discord.js": "^11.5.1", + "discord.js": "^12.4.1", "dotenv": "^8.2.0", "erlpack": "github:hammerandchisel/erlpack", "escape-string-regexp": "^2.0.0", @@ -36,4 +36,4 @@ "eslint-plugin-node": "^10.0.0", "tape": "^4.11.0" } -} +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index c1a06e1..0688216 100644 --- a/src/index.js +++ b/src/index.js @@ -59,15 +59,16 @@ if (statsdHost) { // and from a channel topic update, or from two channel topic updates in quick succession. class DiscordEventQueue { - constructor() { + constructor(id = "global") { + this.id = id; this.size = 0; this.curr = Promise.resolve(); this._onSizeChanged(); } _onSizeChanged() { - if (statsdClient != null) { - statsdClient.send("discord.queuesize", this.size, "g"); + if (VERBOSE) { + console.log(`Event queue [${this.id}] size: ${this.size}`); } } @@ -75,15 +76,52 @@ class DiscordEventQueue { enqueue(fn) { this.size += 1; this._onSizeChanged(); - return this.curr = this.curr.then(_ => fn()).catch(e => console.error(ts(e.stack))).finally(() => { - this.size -= 1; - this._onSizeChanged(); + return (this.curr = this.curr + .then(_ => fn()) + .catch(e => { + console.error(ts(e.stack)); + }) + .finally(() => { + this.size -= 1; + this._onSizeChanged(); + })); + } + +} + +// Creates global an per channel event queues +class DiscordEventQueueManager { + constructor() { + this.globalQueue = new DiscordEventQueue(); + this.channelQueue = {}; + } + + enqueue(task, channelId = null) { + let queue = this.globalQueue; + if (channelId) { + if (this.channelQueue[channelId]) { + queue = this.channelQueue[channelId]; + } else { + queue = this.channelQueue[channelId] = new DiscordEventQueue(channelId); + } + } + const ret = queue.enqueue(task).then(() => { + this._queueUpdated(); }); + this._queueUpdated(); + + return ret; } + _queueUpdated() { + if (statsdClient != null) { + const size = this.globalQueue.size + Object.values(this.channelQueue).reduce((acc, item) => acc + item.size, 0); + statsdClient.send("discord.queuesize", size, "g"); + } + } } -const q = new DiscordEventQueue(); +const q = new DiscordEventQueueManager(); // Prepends a timestamp to a string. function ts(str) { @@ -468,7 +506,7 @@ async function start() { // one-time scan through all channels to look for existing bridges console.info(ts(`Scanning channel topics for Hubs hosts: ${HOSTNAMES.join(", ")}`)); { - const textChannels = Array.from(discordClient.channels.filter(ch => ch.type === "text").values()); + const textChannels = discordClient.channels.cache.array().filter(ch => ch.type === "text"); const candidateBridges = findBridges(topicManager, textChannels); for (const [key, channels] of candidateBridges.entries()) { @@ -498,7 +536,7 @@ async function start() { discord.Permissions.FLAGS.READ_MESSAGES, discord.Permissions.FLAGS.READ_MESSAGE_HISTORY ])) { - const pins = await discordCh.fetchPinnedMessages(); + const pins = await discordCh.messages.fetchPinned(); const notifications = pins.filter(msg => { return msg.author.id === discordClient.user.id && NotificationManager.parseTimestamp(msg).isValid(); }); @@ -528,6 +566,21 @@ async function start() { await msg.unpin(); }); + // Gets debug event and some rate limit events in the shape of a 429 message (ie. set channel topic) + discordClient.on("debug", (...args) => { + console.log("Debug: ", ...args); + }); + + // Gets rate limit events for some (not all) routes (ie. get channel pins) + discordClient.on("rateLimit", info => { + console.log(`Rate limit hit:`); + console.log(`\ttimeout: ${info.timeout}`); + console.log(`\tlimit: ${info.limit}`); + console.log(`\tmethod: ${info.method}`); + console.log(`\tpath: ${info.path}`); + console.log(`\troute: ${info.route}`); + }); + discordClient.on('webhookUpdate', (discordCh) => { q.enqueue(async () => { const hubState = bridges.getHub(discordCh.id); @@ -549,7 +602,7 @@ async function start() { } } } - }); + }, discordCh.id); }); discordClient.on('channelUpdate', (oldChannel, newChannel) => { @@ -590,7 +643,7 @@ async function start() { const currHubDesc = currHubId != null ? currHubId : "nowhere"; console.error(ts(`Failed to update ${formatDiscordCh(newChannel)} bridge from ${prevHubDesc} to ${currHubDesc}:`), e); } - }); + }, oldChannel.id); }); const HELP_PREFIX = "Hi! I'm the Hubs bot. I connect Discord channels with rooms on Hubs (). Type `!hubs help` for more information."; @@ -770,7 +823,7 @@ async function start() { return discordCh.send(COMMAND_HELP_TEXT); } if (args.length === 3) { // !hubs notify clear - const pins = await discordCh.fetchPinnedMessages(); + const pins = await discordCh.messages.fetchPinned(); const notifications = pins.filter(msg => { return msg.author.id === discordClient.user.id && NotificationManager.parseTimestamp(msg).isValid(); }); @@ -793,6 +846,7 @@ async function start() { if (await tryPin(discordCh, msg) != null) { notificationManager.add(when, msg); } + return; } } @@ -813,7 +867,7 @@ async function start() { } - }); + }, discordCh.id); }); }