From d5cbe7752b32681f576dc7233ccaf20acb0b0c6f Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Wed, 19 Mar 2025 22:09:36 -0400 Subject: [PATCH 01/13] updated packages, restructured the roles in the db --- package-lock.json | 1013 +++++++++++++++++++++++++------------------ src/helpers.ts | 55 ++- src/interactions.ts | 12 +- 3 files changed, 633 insertions(+), 447 deletions(-) diff --git a/package-lock.json b/package-lock.json index a83fd56..8201f1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vibecheque", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vibecheque", - "version": "0.0.1", + "version": "0.0.2", "license": "CC0-1.0", "dependencies": { "discord.js": "^14.16.3", @@ -54,9 +54,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", "dev": true, "license": "MIT", "engines": { @@ -64,22 +64,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -95,14 +95,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.10.tgz", + "integrity": "sha512-rRHT8siFIXQrAYOYqZQVsAr8vJ+cBNqcVAY6m5V8/4QqzaPl+zDBe6cLEPRDuNOUf3ww8RfJVlOyQMoSI+5Ang==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.10", + "@babel/types": "^7.26.10", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -112,13 +112,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -161,9 +161,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -201,27 +201,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", + "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz", + "integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.10" }, "bin": { "parser": "bin/babel-parser.js" @@ -470,32 +470,32 @@ } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.10.tgz", + "integrity": "sha512-k8NuDrxr0WrPH5Aupqb2LCVURP/S0vBEn5mK6iH+GIYob66U5EtoZvcdudR2jQ4cmTwhEwW1DLB+Yyas9zjF6A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.10", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -504,9 +504,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz", + "integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -549,32 +549,26 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.9.0.tgz", - "integrity": "sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz", + "integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==", "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.5.0", + "@discordjs/formatters": "^0.6.0", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", + "discord-api-types": "^0.37.119", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" }, "engines": { - "node": ">=18" + "node": ">=16.11.0" }, "funding": { "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/builders/node_modules/discord-api-types": { - "version": "0.37.97", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", - "license": "MIT" - }, "node_modules/@discordjs/collection": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", @@ -585,30 +579,24 @@ } }, "node_modules/@discordjs/formatters": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.5.0.tgz", - "integrity": "sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", "license": "Apache-2.0", "dependencies": { - "discord-api-types": "0.37.97" + "discord-api-types": "^0.37.114" }, "engines": { - "node": ">=18" + "node": ">=16.11.0" }, "funding": { "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/formatters/node_modules/discord-api-types": { - "version": "0.37.97", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", - "license": "MIT" - }, "node_modules/@discordjs/rest": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.0.tgz", - "integrity": "sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz", + "integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.1", @@ -616,10 +604,10 @@ "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", + "discord-api-types": "^0.37.119", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", - "undici": "6.19.8" + "undici": "6.21.1" }, "engines": { "node": ">=18" @@ -640,12 +628,6 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/rest/node_modules/discord-api-types": { - "version": "0.37.97", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.97.tgz", - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==", - "license": "MIT" - }, "node_modules/@discordjs/util": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", @@ -659,20 +641,20 @@ } }, "node_modules/@discordjs/ws": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.1.1.tgz", - "integrity": "sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz", + "integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.3.0", + "@discordjs/rest": "^2.4.3", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", - "discord-api-types": "0.37.83", + "discord-api-types": "^0.37.119", "tslib": "^2.6.2", - "ws": "^8.16.0" + "ws": "^8.17.0" }, "engines": { "node": ">=16.11.0" @@ -693,22 +675,16 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/ws/node_modules/discord-api-types": { - "version": "0.37.83", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.83.tgz", - "integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==", - "license": "MIT" - }, "node_modules/@firebase/analytics": { - "version": "0.10.10", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.10.tgz", - "integrity": "sha512-Psdo7c9g2SLAYh6u1XRA+RZ7ab2JfBVuAt/kLzXkhKZL/gS2cQUCMsOW5p0RIlDPRKqpdNSmvujd2TeRWLKOkQ==", + "version": "0.10.12", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.12.tgz", + "integrity": "sha512-iDCGnw6qdFqwI5ywkgece99WADJNoymu+nLIQI4fZM/vCZ3bEo4wlpEetW71s1HqGpI0hQStiPhqVjFxDb2yyw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/installations": "0.6.11", + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -716,15 +692,15 @@ } }, "node_modules/@firebase/analytics-compat": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.16.tgz", - "integrity": "sha512-Q/s+u/TEMSb2EDJFQMGsOzpSosybBl8HuoSEMyGZ99+0Pu7SIR9MPDGUjc8PKiCFQWDJ3QXxgqh1d/rujyAMbA==", + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.18.tgz", + "integrity": "sha512-Hw9mzsSMZaQu6wrTbi3kYYwGw9nBqOHr47pVLxfr5v8CalsdrG5gfs9XUlPOZjHRVISp3oQrh1j7d3E+ulHPjQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/analytics": "0.10.10", + "@firebase/analytics": "0.10.12", "@firebase/analytics-types": "0.8.3", - "@firebase/component": "0.6.11", - "@firebase/util": "1.10.2", + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -738,14 +714,14 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app": { - "version": "0.10.16", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.16.tgz", - "integrity": "sha512-SUati2qH48gvVGnSsqMkZr1Iq7No52a3tJQ4itboSTM89Erezmw3v1RsfVymrDze9+KiOLmBpvLNKSvheITFjg==", + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.11.2.tgz", + "integrity": "sha512-bFee0hPJZBzNtiizRxdgsu8C9DW3mn1y0OJJ4zHQsccjDYzGOfvN0G3CMGyBIiwNctsFpQa8orbp2IKywoUeqA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -754,14 +730,14 @@ } }, "node_modules/@firebase/app-check": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.10.tgz", - "integrity": "sha512-DWFfxxif/t+Ow4MmRUevDX+A3hVxm1rUf6y5ZP4sIomfnVCO1NNahqtsv9rb1/tKGkTeoVT40weiTS/WjQG1mA==", + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.12.tgz", + "integrity": "sha512-LxjcoIFOU4sgK07ZWb8XDHxuVB+UKs41vPK+Sg9PeZMvEoz84fndFAx8Nz2nipiya2EmyxBgVhff8Hi6GBt+XA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -772,16 +748,16 @@ } }, "node_modules/@firebase/app-check-compat": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.17.tgz", - "integrity": "sha512-a/eadrGsY0MVCBPhrNbKUhoYpms4UKTYLKO7nswwSFVsm3Rw6NslQQCNLfvljcDqP4E7alQDRGJXjkxd/5gJ+Q==", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.19.tgz", + "integrity": "sha512-G8FMiqhrKc4gEEujrBDBBrbRav8MGqoLObWj1hy/riCSg4XlRYhpnq3ev8E9HTirqU1tAGH6oJl7vr+jfM7YNA==", "license": "Apache-2.0", "dependencies": { - "@firebase/app-check": "0.8.10", + "@firebase/app-check": "0.8.12", "@firebase/app-check-types": "0.5.3", - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -804,15 +780,15 @@ "license": "Apache-2.0" }, "node_modules/@firebase/app-compat": { - "version": "0.2.46", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.46.tgz", - "integrity": "sha512-9hSHWE5LMqtKIm13CnH5OZeMPbkVV3y5vgNZ5EMFHcG2ceRrncyNjG9No5XfWQw8JponZdGs4HlE4aMD/jxcFA==", + "version": "0.2.51", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.51.tgz", + "integrity": "sha512-pxF1+coABt+ugqNI0YXDlmkKv4kh3pjI5BqIJJ1VXBo42OZbKMsQbFeos14YBrWwiqqSjUvQ70FBNsv5E2wuxg==", "license": "Apache-2.0", "dependencies": { - "@firebase/app": "0.10.16", - "@firebase/component": "0.6.11", + "@firebase/app": "0.11.2", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -826,14 +802,14 @@ "license": "Apache-2.0" }, "node_modules/@firebase/auth": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.8.1.tgz", - "integrity": "sha512-LX9N/Cf5Z35r5yqm2+5M3+2bRRe/+RFaa/+u4HDni7TA27C/Xm4XHLKcWcLg1BzjrS4zngSaBEOSODvp6RFOqQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.9.1.tgz", + "integrity": "sha512-9KKo5SNVkyJzftsW+daS+PGDbeJ+MFJWXQFHDqqPPH3acWHtiNnGHH5HGpIJErEELrsm9xMPie5zfZ0XpGU8+w==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -850,15 +826,15 @@ } }, "node_modules/@firebase/auth-compat": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.16.tgz", - "integrity": "sha512-YlYwJMBqAyv0ESy3jDUyshMhZlbUiwAm6B6+uUmigNDHU+uq7j4SFiDJEZlFFIz397yBzKn06SUdqutdQzGnCA==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.19.tgz", + "integrity": "sha512-v898POphOIBJliKF76SiGOXh4EdhO5fM6S9a2ZKf/8wHdBea/qwxwZoVVya4DW6Mi7vWyp1lIzHbFgwRz8G9TA==", "license": "Apache-2.0", "dependencies": { - "@firebase/auth": "1.8.1", - "@firebase/auth-types": "0.12.3", - "@firebase/component": "0.6.11", - "@firebase/util": "1.10.2", + "@firebase/auth": "1.9.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -875,9 +851,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/auth-types": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.3.tgz", - "integrity": "sha512-Zq9zI0o5hqXDtKg6yDkSnvMCMuLU6qAVS51PANQx+ZZX5xnzyNLEBO3GZgBUPsV5qIMFhjhqmLDxUqCbnAYy2A==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", "license": "Apache-2.0", "peerDependencies": { "@firebase/app-types": "0.x", @@ -885,12 +861,12 @@ } }, "node_modules/@firebase/component": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.11.tgz", - "integrity": "sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.13.tgz", + "integrity": "sha512-I/Eg1NpAtZ8AAfq8mpdfXnuUpcLxIDdCDtTzWSh+FXnp/9eCKJ3SNbOCKrUCyhLzNa2SiPJYruei0sxVjaOTeg==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -898,15 +874,15 @@ } }, "node_modules/@firebase/data-connect": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.1.2.tgz", - "integrity": "sha512-Bcf29mntFCt5V7aceMe36wnkHrG7cwbMlUVbDHOlh2foQKx9VtSXEONw9r6FtL1sFobHVYOM5L6umX35f59m5g==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.1.tgz", + "integrity": "sha512-PNlfAJ2mcbyRlWfm41nfk8EksTuvMFTFIX+puNzeUa6OTIDtyp1IX1NJVc7n6WpfbErN7tNqcOEMe6BMtpcjVA==", "license": "Apache-2.0", "dependencies": { "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -914,16 +890,16 @@ } }, "node_modules/@firebase/database": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.10.tgz", - "integrity": "sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.13.tgz", + "integrity": "sha512-cdc+LuseKdJXzlrCx8ePMXyctSWtYS9SsP3y7EeA85GzNh/IL0b7HOq0eShridL935iQ0KScZCj5qJtKkGE53g==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "faye-websocket": "0.11.4", "tslib": "^2.1.0" }, @@ -932,16 +908,16 @@ } }, "node_modules/@firebase/database-compat": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.1.tgz", - "integrity": "sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.4.tgz", + "integrity": "sha512-4qsptwZ3DTGNBje56ETItZQyA/HMalOelnLmkC3eR0M6+zkzOHjNHyWUWodW2mqxRKAM0sGkn+aIwYHKZFJXug==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/database": "1.0.10", - "@firebase/database-types": "1.0.7", + "@firebase/component": "0.6.13", + "@firebase/database": "1.0.13", + "@firebase/database-types": "1.0.9", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -949,24 +925,24 @@ } }, "node_modules/@firebase/database-types": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.7.tgz", - "integrity": "sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.9.tgz", + "integrity": "sha512-uCntrxPbJHhZsNRpMhxNCm7GzhYWX+7J2e57wq1ZZ4NJrQw5DORgkAzJMByYZcVAjgADnCxxhK/GkoypH+XpvQ==", "license": "Apache-2.0", "dependencies": { "@firebase/app-types": "0.9.3", - "@firebase/util": "1.10.2" + "@firebase/util": "1.11.0" } }, "node_modules/@firebase/firestore": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.5.tgz", - "integrity": "sha512-OO3rHvjC07jL2ITN255xH/UzCVSvh6xG8oTzQdFScQvFbcm1fjCL1hgAdpDZcx3vVcKMV+6ktr8wbllkB8r+FQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.9.tgz", + "integrity": "sha512-uq/bUtHDqJ5ZqPHAJIlNzHpXUtcVYcASz2V6y7UmP1WLlRKEt1yf1OcQW5u8pY2yq7162OnCl5J5mkOdMTMLZw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "@firebase/webchannel-wrapper": "1.0.3", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", @@ -980,15 +956,15 @@ } }, "node_modules/@firebase/firestore-compat": { - "version": "0.3.40", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.40.tgz", - "integrity": "sha512-18HopMN811KYBc9Ptpr1Rewwio0XF09FF3jc5wtV6rGyAs815SlFFw5vW7ZeLd43zv9tlEc2FzM0H+5Vr9ZRxw==", + "version": "0.3.44", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.44.tgz", + "integrity": "sha512-4Lv2TyHEW+FugXPgmQ0ZylSbh9uFuKDP0lCL1hX9cbxXaafhC/Nww+DWokUQ2zZcynjc8fxFunw6Xbd3QHAlgA==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/firestore": "4.7.5", + "@firebase/component": "0.6.13", + "@firebase/firestore": "4.7.9", "@firebase/firestore-types": "3.0.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -1009,16 +985,16 @@ } }, "node_modules/@firebase/functions": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.10.tgz", - "integrity": "sha512-TP+Dzebazhw6+GduBdWn1kOJRFH84G2z+BW3pNVfkpFRkc//+uT1Uw2+dLpMGSSBRG7FrcDG91vcPnOFCzr15w==", + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.3.tgz", + "integrity": "sha512-Wv7JZMUkKLb1goOWRtsu3t7m97uK6XQvjQLPvn8rncY91+VgdU72crqnaYCDI/ophNuBEmuK8mn0/pAnjUeA6A==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", "@firebase/auth-interop-types": "0.2.4", - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -1029,15 +1005,15 @@ } }, "node_modules/@firebase/functions-compat": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.16.tgz", - "integrity": "sha512-FL7EXehiiBisNIR7mlb0i+moyWKLVfcEJgh/Wq6ZV6BdrCObpCTz7w5EvuRIEFX5e9cNL2oWInKg8S5X4HtINg==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.20.tgz", + "integrity": "sha512-iIudmYDAML6n3c7uXO2YTlzra2/J6lnMzmJTXNthvrKVMgNMaseNoQP1wKfchK84hMuSF8EkM4AvufwbJ+Juew==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/functions": "0.11.10", + "@firebase/component": "0.6.13", + "@firebase/functions": "0.12.3", "@firebase/functions-types": "0.6.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -1054,13 +1030,13 @@ "license": "Apache-2.0" }, "node_modules/@firebase/installations": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.11.tgz", - "integrity": "sha512-w8fY8mw6fxJzsZM2ufmTtomopXl1+bn/syYon+Gpn+0p0nO1cIUEVEFrFazTLaaL9q1CaVhc3HmseRTsI3igAA==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.13.tgz", + "integrity": "sha512-6ZpkUiaygPFwgVneYxuuOuHnSPnTA4KefLEaw/sKk/rNYgC7X6twaGfYb0sYLpbi9xV4i5jXsqZ3WO+yaguNgg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/util": "1.10.2", + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -1069,15 +1045,15 @@ } }, "node_modules/@firebase/installations-compat": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.11.tgz", - "integrity": "sha512-SHRgw5LTa6v8LubmJZxcOCwEd1MfWQPUtKdiuCx2VMWnapX54skZd1PkQg0K4l3k+4ujbI2cn7FE6Li9hbChBw==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.13.tgz", + "integrity": "sha512-f/o6MqCI7LD/ulY9gvgkv6w5k6diaReD8BFHd/y/fEdpsXmFWYS/g28GXCB72bRVBOgPpkOUNl+VsMvDwlRKmw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/installations": "0.6.11", + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", "@firebase/installations-types": "0.5.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1106,15 +1082,15 @@ } }, "node_modules/@firebase/messaging": { - "version": "0.12.14", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.14.tgz", - "integrity": "sha512-cSGP34jJswFvME8tdMDkvJvW6T1jEekyMSyq84AMBZ0KEpJbDWuC9n4wKT2lxUm1jaL651iZnn6g51yCl77ICg==", + "version": "0.12.17", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.17.tgz", + "integrity": "sha512-W3CnGhTm6Nx8XGb6E5/+jZTuxX/EK8Vur4QXvO1DwZta/t0xqWMRgO9vNsZFMYBqFV4o3j4F9qK/iddGYwWS6g==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/installations": "0.6.11", + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -1123,14 +1099,14 @@ } }, "node_modules/@firebase/messaging-compat": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.14.tgz", - "integrity": "sha512-r9weK8jTEA2aGiwy0IbMQPnzuJ0DHkOQaMxGJOlU2QZ1a7fh6RHpNtaoM+LKnn6u1NQgmAOWYNr9vezVQEm9zw==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.17.tgz", + "integrity": "sha512-5Q+9IG7FuedusdWHVQRjpA3OVD9KUWp/IPegcv0s5qSqRLBjib7FlAeWxN+VL0Ew43tuPJBY2HKhEecuizmO1Q==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/messaging": "0.12.14", - "@firebase/util": "1.10.2", + "@firebase/component": "0.6.13", + "@firebase/messaging": "0.12.17", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1144,32 +1120,33 @@ "license": "Apache-2.0" }, "node_modules/@firebase/performance": { - "version": "0.6.11", - "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.11.tgz", - "integrity": "sha512-FlkJFeqLlIeh5T4Am3uE38HVzggliDIEFy/fErEc1faINOUFCb6vQBEoNZGaXvRnTR8lh3X/hP7tv37C7BsK9g==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.1.tgz", + "integrity": "sha512-SkEUurawojCjav2V2AXo6BQLDtv02NxgXPLCiAvrkn95IAKI4W/UbLKYQvMbEez/nqvmnucLyklcMlB0Q5a1iw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/installations": "0.6.11", + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", - "tslib": "^2.1.0" + "@firebase/util": "1.11.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" }, "peerDependencies": { "@firebase/app": "0.x" } }, "node_modules/@firebase/performance-compat": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.11.tgz", - "integrity": "sha512-DqeNBy51W2xzlklyC7Ht9JQ94HhTA08PCcM4MDeyG/ol3fqum/+YgtHWQ2IQuduqH9afETthZqLwCZiSgY7hiA==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.14.tgz", + "integrity": "sha512-/crPg0fDqHIx+FjFoEqWxNp+lJSF40ZG7x43AAJGRaUaWLJDncQm3UJB5/mABaRZb7obs1CQAcRtd4phZFkmZg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/performance": "0.6.11", + "@firebase/performance": "0.7.1", "@firebase/performance-types": "0.2.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1183,15 +1160,15 @@ "license": "Apache-2.0" }, "node_modules/@firebase/remote-config": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.11.tgz", - "integrity": "sha512-9z0rgKuws2nj+7cdiqF+NY1QR4na6KnuOvP+jQvgilDOhGtKOcCMq5XHiu66i73A9kFhyU6QQ2pHXxcmaq1pBw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.0.tgz", + "integrity": "sha512-Yrk4l5+6FJLPHC6irNHMzgTtJ3NfHXlAXVChCBdNFtgmzyGmufNs/sr8oA0auEfIJ5VpXCaThRh3P4OdQxiAlQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/installations": "0.6.11", + "@firebase/component": "0.6.13", + "@firebase/installations": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1199,16 +1176,16 @@ } }, "node_modules/@firebase/remote-config-compat": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.11.tgz", - "integrity": "sha512-zfIjpwPrGuIOZDmduukN086qjhZ1LnbJi/iYzgua+2qeTlO0XdlE1v66gJPwygGB3TOhT0yb9EiUZ3nBNttMqg==", + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.13.tgz", + "integrity": "sha512-UmHoO7TxAEJPIZf8e1Hy6CeFGMeyjqSCpgoBkQZYXFI2JHhzxIyDpr8jVKJJN1dmAePKZ5EX7dC13CmcdTOl7Q==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/remote-config": "0.4.11", - "@firebase/remote-config-types": "0.3.3", - "@firebase/util": "1.10.2", + "@firebase/remote-config": "0.6.0", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1216,19 +1193,19 @@ } }, "node_modules/@firebase/remote-config-types": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.3.tgz", - "integrity": "sha512-YlRI9CHxrk3lpQuFup9N1eohpwdWayKZUNZ/YeQ0PZoncJ66P32UsKUKqVXOaieTjJIOh7yH8JEzRdht5s+d6g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", "license": "Apache-2.0" }, "node_modules/@firebase/storage": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.4.tgz", - "integrity": "sha512-b1KaTTRiMupFurIhpGIbReaWev0k5O3ouTHkAPcEssT+FvU3q/1JwzvkX4+ZdB60Fc43Mbp8qQ1gWfT0Z2FP9Q==", + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.7.tgz", + "integrity": "sha512-FkRyc24rK+Y6EaQ1tYFm3TevBnnfSNA0VyTfew2hrYyL/aYfatBg7HOgktUdB4kWMHNA9VoTotzZTGoLuK92wg==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/util": "1.10.2", + "@firebase/component": "0.6.13", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -1239,15 +1216,15 @@ } }, "node_modules/@firebase/storage-compat": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.14.tgz", - "integrity": "sha512-Ok5FmXJiapaNAOQ8W8qppnfwgP8540jw2B8M0c4TFZqF4BD+CoKBxW0dRtOuLNGadLhzqqkDZZZtkexxrveQqA==", + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.17.tgz", + "integrity": "sha512-CBlODWEZ5b6MJWVh21VZioxwxNwVfPA9CAdsk+ZgVocJQQbE2oDW1XJoRcgthRY1HOitgbn4cVrM+NlQtuUYhw==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.11", - "@firebase/storage": "0.13.4", + "@firebase/component": "0.6.13", + "@firebase/storage": "0.13.7", "@firebase/storage-types": "0.8.3", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -1268,9 +1245,10 @@ } }, "node_modules/@firebase/util": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.2.tgz", - "integrity": "sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.0.tgz", + "integrity": "sha512-PzSrhIr++KI6y4P6C/IdgBNMkEx0Ex6554/cYd0Hm+ovyFSJtJXqb/3OSIdnBoa2cpwZT1/GW56EmRc5qEc5fQ==", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -1280,15 +1258,15 @@ } }, "node_modules/@firebase/vertexai": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.0.1.tgz", - "integrity": "sha512-f48MGSofhaS05ebpN7zMIv4tBqYf19pXr5/4njKtNZVLbjxUswDma0SuFDoO+IwgbdkhFxgtNctM+C1zfI/O1Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/vertexai/-/vertexai-1.1.0.tgz", + "integrity": "sha512-K8CgIFKJrfrf5lYhKnDXOu08FEmIzVExK+ApUZx4Bw2GAmLEA3wDVrsjuupuvpXZSp8QlzvEiXwqshqqc4v0pA==", "license": "Apache-2.0", "dependencies": { "@firebase/app-check-interop-types": "0.3.3", - "@firebase/component": "0.6.11", + "@firebase/component": "0.6.13", "@firebase/logger": "0.4.4", - "@firebase/util": "1.10.2", + "@firebase/util": "1.11.0", "tslib": "^2.1.0" }, "engines": { @@ -1656,9 +1634,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "license": "MIT", "dependencies": { @@ -1808,9 +1786,9 @@ "license": "BSD-3-Clause" }, "node_modules/@sapphire/async-queue": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.3.tgz", - "integrity": "sha512-x7zadcfJGxFka1Q3f8gCts1F0xMwCKbZweM85xECGI0hBTeIZJGGCrHgLggihBoprlQ/hBmDR5LKfIPqnmHM3w==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", "license": "MIT", "engines": { "node": ">=v14.0.0", @@ -1989,18 +1967,18 @@ } }, "node_modules/@types/node": { - "version": "22.7.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", - "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.20.0" } }, "node_modules/@types/node-fetch": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", - "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", "license": "MIT", "dependencies": { "@types/node": "*", @@ -2015,9 +1993,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -2063,9 +2041,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -2089,9 +2067,9 @@ } }, "node_modules/agentkeepalive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", - "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", "license": "MIT", "dependencies": { "humanize-ms": "^1.2.1" @@ -2351,9 +2329,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -2371,9 +2349,9 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -2413,6 +2391,19 @@ "dev": true, "license": "MIT" }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2434,9 +2425,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001677", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", - "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", + "version": "1.0.30001706", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001706.tgz", + "integrity": "sha512-3ZczoTApMAZwPKYWmwVbQMFpXBDds3/0VciVoUwPUbldlYyVLmRVuRs/PcUZtHpbLRpzzDvrvnFuREsGt6lUug==", "dev": true, "funding": [ { @@ -2522,9 +2513,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", - "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, "license": "MIT" }, @@ -2658,9 +2649,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "license": "MIT", "dependencies": { @@ -2752,29 +2743,29 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.100", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.100.tgz", - "integrity": "sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==", + "version": "0.37.119", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz", + "integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==", "license": "MIT" }, "node_modules/discord.js": { - "version": "14.16.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.16.3.tgz", - "integrity": "sha512-EPCWE9OkA9DnFFNrO7Kl1WHHDYFXu3CNVFJg63bfU7hVtjZGyhShwZtSBImINQRWxWP2tgo2XI+QhdXx28r0aA==", + "version": "14.18.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz", + "integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==", "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.9.0", + "@discordjs/builders": "^1.10.1", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.5.0", - "@discordjs/rest": "^2.4.0", + "@discordjs/formatters": "^0.6.0", + "@discordjs/rest": "^2.4.3", "@discordjs/util": "^1.1.1", - "@discordjs/ws": "1.1.1", + "@discordjs/ws": "^1.2.1", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.100", + "discord-api-types": "^0.37.119", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "tslib": "^2.6.3", - "undici": "6.19.8" + "undici": "6.21.1" }, "engines": { "node": ">=18" @@ -2784,9 +2775,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2795,6 +2786,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -2812,9 +2817,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.52", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", - "integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==", + "version": "1.5.120", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.120.tgz", + "integrity": "sha512-oTUp3gfX1gZI+xfD2djr2rzQdHCwHzPQrrK0CD7WpTdF0nPdQ/INcRVjWgLdCT4a9W3jFObR9DAfsuyFQnI8CQ==", "dev": true, "license": "ISC" }, @@ -2847,6 +2852,51 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2946,16 +2996,16 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -2969,9 +3019,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -3059,49 +3109,50 @@ } }, "node_modules/firebase": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.0.2.tgz", - "integrity": "sha512-w4T8BSJpzdZA25QRch5ahLsgB6uRvg1LEic4BaC5rTD1YygroI1AXp+W+rbMnr8d8EjfAv6t4k8doULIjc1P8Q==", + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.4.0.tgz", + "integrity": "sha512-Z6kwhWIPDgIm0+NUEQxwjH14hMP7t42WSFnf/78R0Vh59VovLYTOCTM3MIdY3jlSZ9uKz56FhXrvsNXNhAn/Xg==", "license": "Apache-2.0", "dependencies": { - "@firebase/analytics": "0.10.10", - "@firebase/analytics-compat": "0.2.16", - "@firebase/app": "0.10.16", - "@firebase/app-check": "0.8.10", - "@firebase/app-check-compat": "0.3.17", - "@firebase/app-compat": "0.2.46", + "@firebase/analytics": "0.10.12", + "@firebase/analytics-compat": "0.2.18", + "@firebase/app": "0.11.2", + "@firebase/app-check": "0.8.12", + "@firebase/app-check-compat": "0.3.19", + "@firebase/app-compat": "0.2.51", "@firebase/app-types": "0.9.3", - "@firebase/auth": "1.8.1", - "@firebase/auth-compat": "0.5.16", - "@firebase/data-connect": "0.1.2", - "@firebase/database": "1.0.10", - "@firebase/database-compat": "2.0.1", - "@firebase/firestore": "4.7.5", - "@firebase/firestore-compat": "0.3.40", - "@firebase/functions": "0.11.10", - "@firebase/functions-compat": "0.3.16", - "@firebase/installations": "0.6.11", - "@firebase/installations-compat": "0.2.11", - "@firebase/messaging": "0.12.14", - "@firebase/messaging-compat": "0.2.14", - "@firebase/performance": "0.6.11", - "@firebase/performance-compat": "0.2.11", - "@firebase/remote-config": "0.4.11", - "@firebase/remote-config-compat": "0.2.11", - "@firebase/storage": "0.13.4", - "@firebase/storage-compat": "0.3.14", - "@firebase/util": "1.10.2", - "@firebase/vertexai": "1.0.1" + "@firebase/auth": "1.9.1", + "@firebase/auth-compat": "0.5.19", + "@firebase/data-connect": "0.3.1", + "@firebase/database": "1.0.13", + "@firebase/database-compat": "2.0.4", + "@firebase/firestore": "4.7.9", + "@firebase/firestore-compat": "0.3.44", + "@firebase/functions": "0.12.3", + "@firebase/functions-compat": "0.3.20", + "@firebase/installations": "0.6.13", + "@firebase/installations-compat": "0.2.13", + "@firebase/messaging": "0.12.17", + "@firebase/messaging-compat": "0.2.17", + "@firebase/performance": "0.7.1", + "@firebase/performance-compat": "0.2.14", + "@firebase/remote-config": "0.6.0", + "@firebase/remote-config-compat": "0.2.13", + "@firebase/storage": "0.13.7", + "@firebase/storage-compat": "0.3.17", + "@firebase/util": "1.11.0", + "@firebase/vertexai": "1.1.0" } }, "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -3152,7 +3203,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3177,6 +3227,30 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3187,6 +3261,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3264,6 +3351,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3281,11 +3380,37 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3302,9 +3427,9 @@ "license": "MIT" }, "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", "license": "MIT" }, "node_modules/human-signals": { @@ -3410,9 +3535,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -3522,9 +3647,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -4080,9 +4205,9 @@ } }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -4215,9 +4340,9 @@ } }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -4313,9 +4438,9 @@ "license": "MIT" }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", "license": "Apache-2.0" }, "node_modules/lru-cache": { @@ -4351,9 +4476,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -4380,6 +4505,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4526,9 +4660,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -4581,9 +4715,9 @@ } }, "node_modules/openai": { - "version": "4.68.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.68.1.tgz", - "integrity": "sha512-C9XmYRHgra1U1G4GGFNqRHQEjxhoOWbQYR85IibfJ0jpHUhOm4/lARiKaC/h3zThvikwH9Dx/XOKWPNVygIS3g==", + "version": "4.88.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.88.0.tgz", + "integrity": "sha512-Ll2ZJCdX/56WcCF/wLtAFou+zWRyLeneoXy+qya5T5/wm5LkIr6heJfSn53c5ujXWPB+24cgumiOetbFqcppFA==", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", @@ -4598,18 +4732,22 @@ "openai": "bin/cli" }, "peerDependencies": { + "ws": "^8.18.0", "zod": "^3.23.8" }, "peerDependenciesMeta": { + "ws": { + "optional": true + }, "zod": { "optional": true } } }, "node_modules/openai/node_modules/@types/node": { - "version": "18.19.57", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.57.tgz", - "integrity": "sha512-I2ioBd/IPrYDMv9UNR5NlPElOZ68QB7yY5V2EsLtSrTO0LM0PnCEFF9biLWHf5k+sIy4ohueCV9t4gk1AEdlVA==", + "version": "18.19.80", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz", + "integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -4936,19 +5074,22 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4977,9 +5118,9 @@ } }, "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "license": "MIT", "engines": { @@ -4987,9 +5128,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -5276,9 +5417,9 @@ "license": "MIT" }, "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz", + "integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==", "dev": true, "license": "MIT", "dependencies": { @@ -5289,7 +5430,7 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.6.3", + "semver": "^7.7.1", "yargs-parser": "^21.1.1" }, "bin": { @@ -5325,9 +5466,9 @@ } }, "node_modules/ts-jest/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, "license": "ISC", "bin": { @@ -5388,9 +5529,9 @@ } }, "node_modules/tsc-alias": { - "version": "1.8.10", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.10.tgz", - "integrity": "sha512-Ibv4KAWfFkFdKJxnWfVtdOmB0Zi1RJVxcbPGiCDsFpCQSsmpWyuzHG3rQyI5YkobWwxFPEyQfu1hdo4qLG2zPw==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.11.tgz", + "integrity": "sha512-2DuEQ58A9Rj2NE2c1+/qaGKlshni9MCK95MJzRGhQG0CYLw0bE/ACgbhhTSf/p1svLelwqafOd8stQate2bYbg==", "license": "MIT", "dependencies": { "chokidar": "^3.5.3", @@ -5405,9 +5546,9 @@ } }, "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-detect": { @@ -5434,9 +5575,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5447,24 +5588,24 @@ } }, "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "version": "6.21.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", + "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==", "license": "MIT", "engines": { "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -5483,7 +5624,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -5533,6 +5674,12 @@ "node": ">= 14" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -5627,9 +5774,9 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/src/helpers.ts b/src/helpers.ts index 55beb89..ec8860a 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -6,22 +6,58 @@ import { ref, set, get, child, remove } from "firebase/database"; // maybe only use db to store which roles are moods /** - * determines whether a role should be removed from a given guild + * adds a role to the database under a given guild * - * @param interaction the interaction containing the guild information - * @param roleName the name of the role being modified - * @returns a string promise describing the outcome + * @param guildId the ID of the server containing the role + * @param roleName the name of the role to add to the database + * @returns a string containing the status of the operation */ -export async function updateOldRoleInServer(interaction: ChatInputCommandInteraction, roleName: string | undefined): Promise{ +export async function addRoleToDatabase(guildId: string, roleName: string): Promise { // if no role is given, exit if (roleName === "" || roleName === null || roleName === undefined){ - return "No role specified" + return "Invalid role specified"; } + + let rolesReference = ref(db, `servers/${guildId}/roles`); + + return await set(child(rolesReference, roleName), true).then((): string => { + return "role successfully set" + }).catch((): string => { + return "something went wrong"; + }); +} - // if the guild is invalid, exit - if (!interaction.guild){ - return "No server found"; +/** + * removes a role from the VC database associated with a given guild + * + * @param guildId the ID of the server containing the role + * @param roleName the name of the role to add to the database + * @returns a string containing the status of the operation + */ +export async function removeRoleFromDatabase(guildId: string, roleName: string): Promise { + // if no role is given, exit + if (roleName === "" || roleName === null || roleName === undefined){ + return "Invalid role specified"; } + + let rolesReference = ref(db, `servers/${guildId}/roles`); + + return await remove(child(rolesReference, roleName)).then((): string => { + return "role successfully removed" + }).catch((): string => { + return "something went wrong"; + }); +} + +/** + * determines whether a role should be removed from a given guild + * + * @param interaction the interaction containing the guild information + * @param roleName the name of the role being modified + * @returns a string promise describing the outcome + */ +export async function updateOldRoleInServer(interaction: ChatInputCommandInteraction, roleName: string | undefined): Promise{ + // retrieve database info // TODO: remove the child() call, and add the path here @@ -50,6 +86,7 @@ export async function updateOldRoleInServer(interaction: ChatInputCommandInterac let roleCount = roleObject["count"]; if (roleCount > 1){ // decrement the count + await set(ref(db, 'servers/' + interaction.guildId + '/roles/' + roleName), { count: roleCount - 1 }) diff --git a/src/interactions.ts b/src/interactions.ts index 0868ea9..7e9033c 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -7,7 +7,7 @@ import { import { analyzeTone, analyzeMoodColor } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; -import { updateOldRoleInServer, updateNewRoleInServer} from "./helpers" +import { updateOldRoleInServer, updateNewRoleInServer, addRoleToDatabase, removeRoleFromDatabase} from "./helpers" /** * the callback to a `ping` interaction @@ -128,8 +128,10 @@ export async function mood(interaction: ChatInputCommandInteraction): // delete the old mood from roles if (interaction.guild?.roles.cache.find(role => role.name === oldMood)) { - if (member.roles.cache.find(role => role.name === oldMood)){ - let oldRole = interaction.guild?.roles.cache.find(role => role.name === oldMood); + let oldRole = interaction.guild?.roles.cache.find(role => role.name === oldMood); + + if (oldRole){ + oldRole!.members.size; member.roles.remove(oldRole!); } } @@ -152,8 +154,8 @@ export async function mood(interaction: ChatInputCommandInteraction): } // Update database with roles - await updateOldRoleInServer(interaction, oldMood); - await updateNewRoleInServer(interaction, currentMood); + await removeRoleFromDatabase(interaction.guildId!, oldMood); + await addRoleToDatabase(interaction.guildId!, currentMood); interaction.reply({ ephemeral: true, From ebe6fc3e1a868f0f4f08ebf4edec93ff62fc88ae Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Wed, 19 Mar 2025 22:10:35 -0400 Subject: [PATCH 02/13] removed unneeded imports --- src/helpers.ts | 132 -------------------------------------------- src/interactions.ts | 2 +- 2 files changed, 1 insertion(+), 133 deletions(-) diff --git a/src/helpers.ts b/src/helpers.ts index ec8860a..80b13bf 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -47,136 +47,4 @@ export async function removeRoleFromDatabase(guildId: string, roleName: string): }).catch((): string => { return "something went wrong"; }); -} - -/** - * determines whether a role should be removed from a given guild - * - * @param interaction the interaction containing the guild information - * @param roleName the name of the role being modified - * @returns a string promise describing the outcome - */ -export async function updateOldRoleInServer(interaction: ChatInputCommandInteraction, roleName: string | undefined): Promise{ - - - // retrieve database info - // TODO: remove the child() call, and add the path here - var dbRef = ref(db); - - // attempt to fetch the data associated with `roleName` - var roleObject = null - await get(child(dbRef, 'servers/' + interaction.guildId + '/roles/' + roleName)).then((snapshot) => { - if (snapshot.exists()) { - roleObject = snapshot.val(); - } else { - console.log("No data available when getting role"); - } - }).catch((error) => { - console.error(error); - }); - - // ensure the role was retrieved properly - if (roleObject === null){ - return "Role object null"; - } - - // if multiple people have the role, decrement the count - // if one person has the role, remove it (since they are the function caller) - // otherwise, no users have the role - let roleCount = roleObject["count"]; - if (roleCount > 1){ - // decrement the count - - await set(ref(db, 'servers/' + interaction.guildId + '/roles/' + roleName), { - count: roleCount - 1 - }) - return "Decreased role count"; - } else if (roleCount === 1){ - // remove the role - return removeOldRoleInServer(interaction, roleName); - } else { - // no role found! - return "No role found"; - } -} - -/** - * attempts to remove a role from a given server - * - * @param interaction the interaction containing the guild information - * @param roleName the name of the role being modified - * @returns a string promise describing the outcome - */ -export async function removeOldRoleInServer(interaction: ChatInputCommandInteraction, roleName: string | undefined): Promise{ - // if no role is given, exit - if (roleName === "" || roleName === null || roleName === undefined){ - return "No role specified" - } - - // if the guild is invalid, exit - if (!interaction.guild){ - return "No server found"; - } - - // remove the role in discord - interaction.guild.roles.cache.find(role => role.name === roleName)!.delete(); - - // remove the role in the db - await remove(ref(db, 'servers/' + interaction.guildId + '/roles/' + roleName)); - - // return successful message - return "Removed role"; -} - -/** - * determines whether a role should be removed from a given guild or simply incremented - * - * @param interaction the interaction containing the guild information - * @param roleName the name of the role being modified - * @returns a string promise describing the outcome - */ -export async function updateNewRoleInServer(interaction: ChatInputCommandInteraction, roleName: string | undefined): Promise{ - // if no role is given, exit - if (roleName === "" || roleName === null || roleName === undefined){ - return "No role specified" - } - - // retrieve database info - // TODO: remove the child() call, and add the path here - var dbRef = ref(db); - - // if the guild is invalid, exit - if (!interaction.guild){ - return "No server found"; - } - - // attempt to access the role data in the db - var roleObject = null - await get(child(dbRef, 'servers/' + interaction.guildId + '/roles/' + roleName)).then((snapshot) => { - if (snapshot.exists()) { - roleObject = snapshot.val(); - } else { - console.log("No data available when getting role"); - } - }).catch((error) => { - console.error(error); - }); - - // if the role exists, increment it - // if not, create it - if (roleObject !== null){ - // increment the role count - let roleCount = roleObject["count"]; - await set(ref(db, 'servers/' + interaction.guildId + '/roles/' + roleName), { - count: roleCount + 1 - }) - } else { - // register the role with count 1 - await set(ref(db, 'servers/' + interaction.guildId + '/roles/' + roleName), { - count: 1 - }) - } - - // return a successful message - return "Updated role count"; } \ No newline at end of file diff --git a/src/interactions.ts b/src/interactions.ts index 7e9033c..05c021f 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -7,7 +7,7 @@ import { import { analyzeTone, analyzeMoodColor } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child } from "firebase/database"; -import { updateOldRoleInServer, updateNewRoleInServer, addRoleToDatabase, removeRoleFromDatabase} from "./helpers" +import { addRoleToDatabase, removeRoleFromDatabase} from "./helpers" /** * the callback to a `ping` interaction From d828e515b6294d52e26a66a8aa5506aa171818c6 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Thu, 20 Mar 2025 15:30:34 -0400 Subject: [PATCH 03/13] bot now cleans up mood roles on launch --- src/discordApp.ts | 11 ++++++- src/gptRequests.ts | 2 +- src/helpers.ts | 71 +++++++++++++++++++++++++++++++++++++++------ src/interactions.ts | 56 +++++++++++++++++------------------ 4 files changed, 101 insertions(+), 39 deletions(-) diff --git a/src/discordApp.ts b/src/discordApp.ts index d12f30c..1a270f3 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,6 +1,7 @@ import "dotenv/config"; import { Client, GatewayIntentBits, Events } from "discord.js"; import { clarify, embed, ping, tone, requestAnonymousClarification, mood } from "./interactions" +import { cleanupRoles } from "./helpers"; export async function launchBot(): Promise { // the client has to declare the features it uses up front so discord.js kno9ws if it can @@ -9,7 +10,8 @@ export async function launchBot(): Promise { // discord API: https://discord.com/developers/docs/topics/gateway#list-of-intents const client = new Client({ intents: [ - GatewayIntentBits.GuildEmojisAndStickers, + GatewayIntentBits.GuildExpressions, + GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, @@ -25,6 +27,13 @@ export async function launchBot(): Promise { client.on(Events.ClientReady, () => { if (client.user) { console.log(`client "ready": Logged in as ${client.user.tag}!`); + + client.guilds.fetch().then(guilds => { + guilds.map((_, id) => { + console.log(`cleaning up roles in guild with id ${id}`); + cleanupRoles(client, id); + }) + }); } else { console.error(`client "ready": client.user is null!`); } diff --git a/src/gptRequests.ts b/src/gptRequests.ts index 575de37..113d724 100644 --- a/src/gptRequests.ts +++ b/src/gptRequests.ts @@ -72,7 +72,7 @@ export async function analyzeMoodColor(mood: string): Promise { "text": ` This text is supposed to show a text messaging app user's mood. The text might be an emotion, or a symbol for an emotion (such as a smiley) or it could be something arbitrary. Try to come up with a color hexcode that depicts the mood, and return that as hexcode without the pound (#) symbol - (example, 000000) Only return the 6 character hexcode that is appropriate for the mood, and nothing else. + (example, 000000) Only return the 6 character hexcode that is appropriate for the mood, and nothing else. Use saturated colors. ` } ] diff --git a/src/helpers.ts b/src/helpers.ts index 80b13bf..48bc025 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,9 +1,21 @@ -import { CacheType, ChatInputCommandInteraction} from "discord.js"; +import { CacheType, ChatInputCommandInteraction, Client, FetchMembersOptions, Guild, Role, Snowflake} from "discord.js"; import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child, remove } from "firebase/database"; -// TODO: remove db dependancy, use interaction.guild.roles.cache.find(role => role.members.size) to find the number of users with a role -// maybe only use db to store which roles are moods +// the minimum amount of time a mood can be in a server before it gets deleted automatically +export const MINIMUM_MOOD_LIFESPAN: number = 3 * (1000 * 60); + +/** + * extracts the timestamp field of discord's Snowflake structure + * + * https://discord.com/developers/docs/reference#snowflakes + * + * @param snowflake the snowflake to extract the timestamp from + * @returns the timestamp of the snowflake + */ +export function getTimestampFromSnowflake(snowflake: Snowflake): number { + return (parseInt(snowflake) >> 22) + 1420070400000; +} /** * adds a role to the database under a given guild @@ -12,15 +24,15 @@ import { ref, set, get, child, remove } from "firebase/database"; * @param roleName the name of the role to add to the database * @returns a string containing the status of the operation */ -export async function addRoleToDatabase(guildId: string, roleName: string): Promise { +export async function addRoleToDatabase(guildId: string, role: Role): Promise { // if no role is given, exit - if (roleName === "" || roleName === null || roleName === undefined){ + if (!role){ return "Invalid role specified"; } let rolesReference = ref(db, `servers/${guildId}/roles`); - return await set(child(rolesReference, roleName), true).then((): string => { + return await set(child(rolesReference, role.name), role.id).then((): string => { return "role successfully set" }).catch((): string => { return "something went wrong"; @@ -34,17 +46,58 @@ export async function addRoleToDatabase(guildId: string, roleName: string): Prom * @param roleName the name of the role to add to the database * @returns a string containing the status of the operation */ -export async function removeRoleFromDatabase(guildId: string, roleName: string): Promise { +export async function removeRoleFromDatabase(guildId: string, role: Role): Promise { // if no role is given, exit - if (roleName === "" || roleName === null || roleName === undefined){ + if (!role){ return "Invalid role specified"; } let rolesReference = ref(db, `servers/${guildId}/roles`); - return await remove(child(rolesReference, roleName)).then((): string => { + return await remove(child(rolesReference, role.name)).then((): string => { return "role successfully removed" }).catch((): string => { return "something went wrong"; }); +} + +/** + * removes a role from the VC database associated with a given guild + * + * @param guildId the ID of the server to clean + * @returns a string containing the status of the operation + */ +export async function cleanupRoles(client: Client, guildId: string): Promise { + let guild = await client.guilds.fetch(guildId); + // if no role is given, exit + if (guild){ + + let rolesReference = ref(db, `servers/${guildId}/roles`); + + let snapshot = await get(rolesReference); + + snapshot.forEach((roleSnapshot) => { + let roleSnowflake: Snowflake = roleSnapshot.val(); + + let timeDifference = Date.now() - getTimestampFromSnowflake(roleSnowflake); + + if (timeDifference >= MINIMUM_MOOD_LIFESPAN) { + guild.roles.fetch(roleSnowflake, {cache: false, force: true}).then(role => { + if (!role) return; + + guild.members.fetch().then(members => { + if (!members.some(member => member.roles.cache.some(memberRole => memberRole.id === roleSnowflake))) { + console.log(`removing role ${roleSnowflake} (${roleSnapshot.key})`); + role.delete("Role out of use"); + removeRoleFromDatabase(guildId, role); + } + }); + }); + } + }); + + return "all unused roles removed"; + } else { + return "Bot does not have access to the specified guild"; + } } \ No newline at end of file diff --git a/src/interactions.ts b/src/interactions.ts index 05c021f..cb09d58 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -2,11 +2,13 @@ import { CacheType, ChatInputCommandInteraction, EmbedBuilder, - MessageContextMenuCommandInteraction + MessageContextMenuCommandInteraction, + Role, + Snowflake } from "discord.js"; import { analyzeTone, analyzeMoodColor } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file -import { ref, set, get, child } from "firebase/database"; +import { ref, set, get, child, query } from "firebase/database"; import { addRoleToDatabase, removeRoleFromDatabase} from "./helpers" /** @@ -22,12 +24,12 @@ export async function ping(interaction: ChatInputCommandInteraction): return new Promise((resolve, reject) => { // reply after 1 second setTimeout(() => { - if (interaction.isRepliable()) { - interaction.editReply(`pong!`); - resolve(); - } else { - reject(); - } + if (interaction.isRepliable()) { + interaction.editReply(`pong!`); + resolve(); + } else { + reject(); + } }, 1000); }) } @@ -108,7 +110,7 @@ Here's a short list of tones: \`\` (***TODO***)`); */ export async function mood(interaction: ChatInputCommandInteraction): Promise { var currentMood = interaction.options.getString('currentmood')!; - let oldMood = ""; + let oldMood: Snowflake | undefined = undefined; // Get the old mood from database var dbRef = ref(db); @@ -125,37 +127,35 @@ export async function mood(interaction: ChatInputCommandInteraction): let guild = interaction.guild!; let member = await guild.members.fetch(interaction.user.id); - + // delete the old mood from roles - if (interaction.guild?.roles.cache.find(role => role.name === oldMood)) { - let oldRole = interaction.guild?.roles.cache.find(role => role.name === oldMood); - - if (oldRole){ - oldRole!.members.size; - member.roles.remove(oldRole!); - } - } + guild.roles.fetch(oldMood).then(role => { + if (role) member.roles.remove(role); + }).catch((error) => { + console.error(error); + });; - // set the new mood in database - await set(ref(db, 'servers/' + interaction.guildId + '/username/' + interaction.user.id), { - mood: currentMood, - timestamp: interaction.createdTimestamp - }); + let newRole: Role | undefined; // update new role if (interaction.guild?.roles.cache.find(role => role.name === currentMood)) { - let newRole = guild.roles.cache.find((r) => r.name === currentMood); + newRole = guild.roles.cache.find((r) => r.name === currentMood); member.roles.add(newRole!); } else { let moodColorHex = await analyzeMoodColor(currentMood); await guild.roles.create({name: currentMood, color: `#${moodColorHex}`}) - let newRole = guild.roles.cache.find((r) => r.name === currentMood); + newRole = guild.roles.cache.find((r) => r.name === currentMood); member.roles.add(newRole!); } - // Update database with roles - await removeRoleFromDatabase(interaction.guildId!, oldMood); - await addRoleToDatabase(interaction.guildId!, currentMood); + // set the new mood in database + await set(ref(db, 'servers/' + interaction.guildId + '/username/' + interaction.user.id), { + mood: newRole!.id, + timestamp: interaction.createdTimestamp + }); + + // Update database with new role + await addRoleToDatabase(interaction.guildId!, newRole!); interaction.reply({ ephemeral: true, From 8ebb783fc28cc0571d178aba19150384942f66c9 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Thu, 20 Mar 2025 15:59:13 -0400 Subject: [PATCH 04/13] restructured role removal code --- src/discordApp.ts | 4 ++-- src/helpers.ts | 53 ++++++++++++++++++++++++++++++--------------- src/interactions.ts | 20 ++++++++++------- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/src/discordApp.ts b/src/discordApp.ts index 1a270f3..9639c81 100644 --- a/src/discordApp.ts +++ b/src/discordApp.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import { Client, GatewayIntentBits, Events } from "discord.js"; import { clarify, embed, ping, tone, requestAnonymousClarification, mood } from "./interactions" -import { cleanupRoles } from "./helpers"; +import { cleanupMoods } from "./helpers"; export async function launchBot(): Promise { // the client has to declare the features it uses up front so discord.js kno9ws if it can @@ -31,7 +31,7 @@ export async function launchBot(): Promise { client.guilds.fetch().then(guilds => { guilds.map((_, id) => { console.log(`cleaning up roles in guild with id ${id}`); - cleanupRoles(client, id); + cleanupMoods(client, id); }) }); } else { diff --git a/src/helpers.ts b/src/helpers.ts index 48bc025..db59ede 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -3,7 +3,7 @@ import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child, remove } from "firebase/database"; // the minimum amount of time a mood can be in a server before it gets deleted automatically -export const MINIMUM_MOOD_LIFESPAN: number = 3 * (1000 * 60); +export const MINIMUM_MOOD_LIFESPAN: number = 0.5 * (1000 * 60); /** * extracts the timestamp field of discord's Snowflake structure @@ -62,12 +62,43 @@ export async function removeRoleFromDatabase(guildId: string, role: Role): Promi } /** - * removes a role from the VC database associated with a given guild + * removes a role from the VC database and guild if it is unused + * + * @param guildId the ID of the server containing the role + * @param roleName the name of the role to add to the database + * @returns a string containing the status of the operation + */ +export async function removeRoleIfUnused(role: Role | null): Promise { + // if no role is given, exit + if (!role){ + return "Invalid role specified"; + } + + let timeDifference = Date.now() - getTimestampFromSnowflake(role.id); + + if (timeDifference < MINIMUM_MOOD_LIFESPAN) { + return "role is too young to be removed" + } + + return await role.guild.members.fetch().then((members): string => { + if (members.some(member => member.roles.cache.some(memberRole => memberRole.id === role.id))) { + return "role still in use" + } + + console.log(`removing role ${role.id} (${role.name})`); + role.delete("Role out of use"); + removeRoleFromDatabase(role.guild.id, role); + return "role removed" + }); +} + +/** + * removes all unused moods * * @param guildId the ID of the server to clean * @returns a string containing the status of the operation */ -export async function cleanupRoles(client: Client, guildId: string): Promise { +export async function cleanupMoods(client: Client, guildId: string): Promise { let guild = await client.guilds.fetch(guildId); // if no role is given, exit if (guild){ @@ -79,21 +110,7 @@ export async function cleanupRoles(client: Client, guildId: string): Promise { let roleSnowflake: Snowflake = roleSnapshot.val(); - let timeDifference = Date.now() - getTimestampFromSnowflake(roleSnowflake); - - if (timeDifference >= MINIMUM_MOOD_LIFESPAN) { - guild.roles.fetch(roleSnowflake, {cache: false, force: true}).then(role => { - if (!role) return; - - guild.members.fetch().then(members => { - if (!members.some(member => member.roles.cache.some(memberRole => memberRole.id === roleSnowflake))) { - console.log(`removing role ${roleSnowflake} (${roleSnapshot.key})`); - role.delete("Role out of use"); - removeRoleFromDatabase(guildId, role); - } - }); - }); - } + guild.roles.fetch(roleSnowflake).then(removeRoleIfUnused); }); return "all unused roles removed"; diff --git a/src/interactions.ts b/src/interactions.ts index cb09d58..5a46ee9 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -9,7 +9,7 @@ import { import { analyzeTone, analyzeMoodColor } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file import { ref, set, get, child, query } from "firebase/database"; -import { addRoleToDatabase, removeRoleFromDatabase} from "./helpers" +import { addRoleToDatabase, MINIMUM_MOOD_LIFESPAN, removeRoleFromDatabase, removeRoleIfUnused} from "./helpers" /** * the callback to a `ping` interaction @@ -127,13 +127,17 @@ export async function mood(interaction: ChatInputCommandInteraction): let guild = interaction.guild!; let member = await guild.members.fetch(interaction.user.id); - - // delete the old mood from roles - guild.roles.fetch(oldMood).then(role => { - if (role) member.roles.remove(role); - }).catch((error) => { - console.error(error); - });; + if (oldMood) { + // delete the old mood from roles + guild.roles.fetch(oldMood).then(role => { + if (role) { + member.roles.remove(role); + setTimeout(() => removeRoleIfUnused(role), MINIMUM_MOOD_LIFESPAN); + } + }).catch((error) => { + console.error(error); + }); + } let newRole: Role | undefined; From dde632b109f3ce1e936c5ca5182dba0f5f02d0fc Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Thu, 20 Mar 2025 16:46:19 -0400 Subject: [PATCH 05/13] mood command fixed, existing roles protected --- src/interactions.ts | 62 ++++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/src/interactions.ts b/src/interactions.ts index 5a46ee9..4b82a11 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -3,12 +3,13 @@ import { ChatInputCommandInteraction, EmbedBuilder, MessageContextMenuCommandInteraction, + MessageFlags, Role, Snowflake } from "discord.js"; import { analyzeTone, analyzeMoodColor } from "./gptRequests"; import db from './firebase'; // Import from your firebase.ts file -import { ref, set, get, child, query } from "firebase/database"; +import { ref, set, get, child, query, DataSnapshot } from "firebase/database"; import { addRoleToDatabase, MINIMUM_MOOD_LIFESPAN, removeRoleFromDatabase, removeRoleIfUnused} from "./helpers" /** @@ -109,29 +110,51 @@ Here's a short list of tones: \`\` (***TODO***)`); * @returns void */ export async function mood(interaction: ChatInputCommandInteraction): Promise { - var currentMood = interaction.options.getString('currentmood')!; - let oldMood: Snowflake | undefined = undefined; + const currentMood = interaction.options.getString('currentmood')!; + let oldMood: Snowflake | null; + let newRole: Role | undefined; + + await interaction.deferReply({flags: MessageFlags.Ephemeral}); + + // ensure the mood is not an existing server role + newRole = interaction.guild?.roles.cache.find(role => role.name === currentMood); + if (newRole) { + const dbRoleRef = ref(db, `servers/${interaction.guildId}/roles/${newRole.name}`); + const inDB = await get(dbRoleRef).then(snapshot => snapshot.exists()); + + // if the role is not in our database, it should be protected + if (!inDB) { + console.log(`mood change denied, ${newRole.id} (${newRole.name}) is not a VC mood`); + interaction.editReply("That is a preexisting role in this server! Please select a new mood."); + return; + } + } + + // Get a reference to this user's section of the db + const dbUserRef = ref(db, `servers/${interaction.guildId}/username/${interaction.user.id}`); // Get the old mood from database - var dbRef = ref(db); - await get(child(dbRef, 'servers/' + interaction.guildId + '/username/' + interaction.user.id)).then((snapshot) => { + oldMood = await get(dbUserRef).then((snapshot) => { if (snapshot.exists()) { - oldMood = snapshot.val()["mood"] + return oldMood = snapshot.val()["mood"] } else { - console.log("No data available"); + console.log("No data available"); + return null; } }).catch((error) => { console.error(error); }); - let guild = interaction.guild!; - let member = await guild.members.fetch(interaction.user.id); + const guild = interaction.guild!; + const member = await guild.members.fetch(interaction.user.id); + // if they had a db entry (old mood role), attempt to remove it if (oldMood) { - // delete the old mood from roles guild.roles.fetch(oldMood).then(role => { if (role) { member.roles.remove(role); + // wait a bit for the cache to update + // maybe just clean roles after a certain interval of time eventually setTimeout(() => removeRoleIfUnused(role), MINIMUM_MOOD_LIFESPAN); } }).catch((error) => { @@ -139,21 +162,17 @@ export async function mood(interaction: ChatInputCommandInteraction): }); } - let newRole: Role | undefined; - // update new role - if (interaction.guild?.roles.cache.find(role => role.name === currentMood)) { - newRole = guild.roles.cache.find((r) => r.name === currentMood); - member.roles.add(newRole!); + if (newRole) { + member.roles.add(newRole); } else { - let moodColorHex = await analyzeMoodColor(currentMood); - await guild.roles.create({name: currentMood, color: `#${moodColorHex}`}) - newRole = guild.roles.cache.find((r) => r.name === currentMood); + const moodColorHex = await analyzeMoodColor(currentMood); + newRole = await guild.roles.create({name: currentMood, color: `#${moodColorHex}`}); member.roles.add(newRole!); } // set the new mood in database - await set(ref(db, 'servers/' + interaction.guildId + '/username/' + interaction.user.id), { + await set(dbUserRef, { mood: newRole!.id, timestamp: interaction.createdTimestamp }); @@ -161,10 +180,7 @@ export async function mood(interaction: ChatInputCommandInteraction): // Update database with new role await addRoleToDatabase(interaction.guildId!, newRole!); - interaction.reply({ - ephemeral: true, - content: "Thanks for updating your mood!" - }) + interaction.editReply("Thanks for updating your mood!"); } /** From f77047afa1f11f1e84e25f764800083726c0feaf Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Thu, 20 Mar 2025 16:59:10 -0400 Subject: [PATCH 06/13] cleared old test file --- src/helpers.test.ts | 192 ++------------------------------------------ 1 file changed, 7 insertions(+), 185 deletions(-) diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 997aa77..7b21d98 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,6 +1,6 @@ import * as discordJS from "discord.js"; import { MockDiscord } from "./testing/mocks/mockDiscord"; -import { updateOldRoleInServer, removeOldRoleInServer, updateNewRoleInServer } from "./helpers"; +import { getTimestampFromSnowflake } from "./helpers"; jest.mock("discord.js"); type MockDatabase = { @@ -46,192 +46,14 @@ describe("Testing helper functions", () => { jest.spyOn(console, "log").mockImplementation(() => {}); }); - describe("Testing updateOldRoleInServer", () => { - test("returns 'No role specified' when roleName is empty, null, or undefined", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; + describe("Testing getTimestampFromSnowflake", () => { + test("the snowflake '0' should return the Discord Epoch (1420070400000)", () => { + const inputSnowflake = "0"; + const outputTime = 1420070400000; - expect(await updateOldRoleInServer(interaction, "")).toBe("No role specified"); - expect(await updateOldRoleInServer(interaction, undefined)).toBe("No role specified"); - }); - - test("returns 'No server found' when guild is null", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - Object.defineProperty(interaction, 'guild', { value: null }); - - expect(await updateOldRoleInServer(interaction, "test-role")).toBe("No server found"); - }); - - test("prints \"No data available when getting role\" when role does not exist", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const { get: mockGet } = require('firebase/database') as MockDatabase; - mockGet.mockResolvedValueOnce({ - exists: () => false, - val: () => null - }); - - const spyConsole = jest.spyOn(console, "log"); - - await updateOldRoleInServer(interaction, "test-role"); - - expect(spyConsole).toHaveBeenCalledWith("No data available when getting role"); - }); - - test("prints error when getting role fails", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const error = new Error("TEST ERROR"); - - const spyConsoleError = jest.spyOn(console, "error").mockImplementationOnce(() => {}); // Mock implementation to prevent actual error printing - - const { get: mockGet } = require('firebase/database') as MockDatabase; - mockGet.mockRejectedValueOnce(error); - - - await updateOldRoleInServer(interaction, "test-role"); - - expect(spyConsoleError).toHaveBeenCalledWith(error); - }); - - test("decreases role count when count > 1", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const { set: mockSet } = require('firebase/database') as MockDatabase; - - const result = await updateOldRoleInServer(interaction, "test-role"); - - expect(result).toBe("Decreased role count"); - expect(mockSet).toHaveBeenCalledWith("mock-ref", { count: 1 }); - }); - - test("removes role when count = 1", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const { get: mockGet } = require('firebase/database') as MockDatabase; - mockGet.mockResolvedValueOnce({ - exists: () => true, - val: () => ({ count: 1 }) - }); - - const mockRoleDelete = jest.fn(); - - discord.addRoleToGuild("test-role", "000000"); - - jest.spyOn(discord.getRoles(), "find").mockReturnValueOnce({ - delete: mockRoleDelete - }); - - const result = await updateOldRoleInServer(interaction, "test-role"); - - expect(result).toBe("Removed role"); - expect(mockRoleDelete).toHaveBeenCalled(); - }); - }); - - describe("Testing removeOldRoleInServer", () => { - test("returns 'No role specified' when roleName is empty, null, or undefined", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - expect(await removeOldRoleInServer(interaction, "")).toBe("No role specified"); - expect(await removeOldRoleInServer(interaction, undefined)).toBe("No role specified"); - }); - - test("returns 'No server found' when guild is null", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - Object.defineProperty(interaction, 'guild', { value: null }); - - expect(await removeOldRoleInServer(interaction, "test-role")).toBe("No server found"); - }); - - test("removes role from server and database", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - discord.addRoleToGuild("test-role", "000000"); - - const { remove: mockRemove } = require('firebase/database') as MockDatabase; - - const mockRoleDelete = jest.fn(); - - jest.spyOn(discord.getRoles(), "find").mockReturnValueOnce({ - delete: mockRoleDelete - }); - - const result = await removeOldRoleInServer(interaction, "test-role"); - - expect(result).toBe("Removed role"); - expect(mockRemove).toHaveBeenCalledWith("mock-ref"); - expect(mockRoleDelete).toHaveBeenCalled(); - }); - }); - - describe("Testing updateNewRoleInServer", () => { - test("returns 'No role specified' when roleName is empty, null, or undefined", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - expect(await updateNewRoleInServer(interaction, "")).toBe("No role specified"); - expect(await updateNewRoleInServer(interaction, undefined)).toBe("No role specified"); - }); - - test("returns 'No server found' when guild is null", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - Object.defineProperty(interaction, 'guild', { value: null }); - - expect(await updateNewRoleInServer(interaction, "test-role")).toBe("No server found"); - }); - - test("prints error when getting role fails", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const error = new Error("TEST ERROR"); - - const spyConsoleError = jest.spyOn(console, "error").mockImplementationOnce(() => {}); // Mock implementation to prevent actual error printing - - const { get: mockGet } = require('firebase/database') as MockDatabase; - mockGet.mockRejectedValueOnce(error); - - await updateNewRoleInServer(interaction, "test-role"); - - expect(spyConsoleError).toHaveBeenCalledWith(error); - }); - - test("increments existing role count", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const { set: mockSet } = require('firebase/database') as MockDatabase; - - const result = await updateNewRoleInServer(interaction, "test-role"); - - expect(result).toBe("Updated role count"); - expect(mockSet).toHaveBeenCalledWith("mock-ref", { count: 3 }); - }); - - test("creates new role with count 1 when role doesn't exist", async () => { - const discord = new MockDiscord({ command: "/mood" }); - const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; - - const { get: mockGet, set: mockSet } = require('firebase/database') as MockDatabase; - mockGet.mockResolvedValueOnce({ - exists: () => false, - val: () => null - }); - - const result = await updateNewRoleInServer(interaction, "test-role"); + const result = getTimestampFromSnowflake(inputSnowflake); - expect(result).toBe("Updated role count"); - expect(mockSet).toHaveBeenCalledWith("mock-ref", { count: 1 }); + expect(result).toBe(outputTime); }); }); }); \ No newline at end of file From 009a0e4abb37d250dfb3dea55836b0a7b0da3673 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Thu, 20 Mar 2025 17:45:18 -0400 Subject: [PATCH 07/13] about half the tests fixed --- src/__mocks__/discord.js.ts | 22 +++++++++++++++++++++- src/testing/mocks/mockDiscord.ts | 9 +++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/__mocks__/discord.js.ts b/src/__mocks__/discord.js.ts index 76df2f3..194202c 100644 --- a/src/__mocks__/discord.js.ts +++ b/src/__mocks__/discord.js.ts @@ -1,8 +1,9 @@ -import { Client, ClientOptions, RESTOptions, UserFlagsBitField } from "discord.js"; +import { Client, ClientOptions, GuildManager, RESTOptions, UserFlagsBitField } from "discord.js"; const discordJS = jest.requireActual("discord.js"); const mockDiscordJS = jest.createMockFromModule("discord.js"); +mockDiscordJS.MessageFlags = discordJS.MessageFlags; mockDiscordJS.EmbedBuilder = discordJS.EmbedBuilder; mockDiscordJS.ApplicationCommandOptionType = discordJS.ApplicationCommandOptionType; mockDiscordJS.GatewayIntentBits = discordJS.GatewayIntentBits; @@ -32,12 +33,29 @@ class MockREST extends discordJS.REST { } } + +class MockGuildManager { + create: jest.Mock; + fetch: jest.Mock; + setIncidentActions: jest.Mock; + widgetImageURL: jest.Mock; + + constructor() { + this.create = jest.fn().mockResolvedValue(null); + this.fetch = jest.fn().mockResolvedValue(new mockDiscordJS.Collection()); + this.setIncidentActions = jest.fn().mockResolvedValue(null); + this.widgetImageURL = jest.fn().mockResolvedValue(null); + } +} + // @ts-ignore class MockClient { _ready: true = true; on: jest.Mock; options: ClientOptions; token: string; + guilds: GuildManager; + constructor(options?: ClientOptions) { this.on = MockClient.prototype.on; this.login = MockClient.prototype.login; @@ -47,6 +65,7 @@ class MockClient { } as any } this.token = "test-token"; + this.guilds = new MockGuildManager() as unknown as GuildManager; } public login(token: string): Promise { @@ -98,5 +117,6 @@ class MockClientUser { mockDiscordJS.REST = MockREST; mockDiscordJS.Client = MockClient as unknown as typeof discordJS.Client; mockDiscordJS.ClientUser = MockClientUser as unknown as typeof discordJS.ClientUser; +mockDiscordJS.GuildManager = MockGuildManager as unknown as typeof discordJS.GuildManager; module.exports = mockDiscordJS; diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 396e206..64702e7 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -34,7 +34,7 @@ export class MockDiscord { private user!: User; private channel!: Channel; private interaction!: CommandInteraction; - private interactionReply!: string | MessagePayload | InteractionEditReplyOptions; + private interactionReply!: string | MessagePayload | InteractionReplyOptions | InteractionEditReplyOptions; private latestMessage!: string | MessagePayload; private interactionOptions!: CommandInteractionOption; private guild: Guild; @@ -104,6 +104,11 @@ export class MockDiscord { name: "mock guild", roles: { cache: this.roles, + fetch: jest.fn().mockResolvedValue({ + id: "role-id", + name: "role-name", + color: 0x000000 + }), create: jest.fn().mockImplementation((options: any) => { // console.log("create role called with options: " + options.name + " " + options.color); const newRole = { @@ -132,7 +137,7 @@ export class MockDiscord { return this.interaction; } - public getInteractionReply(): string | MessagePayload | InteractionEditReplyOptions { + public getInteractionReply(): string | MessagePayload | InteractionReplyOptions | InteractionEditReplyOptions { return this.interactionReply; } From 3ad93b788bb521fbeaa57e9bbff0a3a34ce25b61 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Fri, 21 Mar 2025 17:14:54 -0400 Subject: [PATCH 08/13] tests passing, now to reintroduce helper.ts tests --- src/__mocks__/firebase/database.ts | 21 +++++++++ src/__mocks__/helpers.ts | 7 +++ src/helpers.test.ts | 39 ++-------------- src/helpers.ts | 9 ++-- src/index.test.ts | 2 +- src/interactions.test.ts | 75 ++++++++++-------------------- src/interactions.ts | 6 ++- src/testing/mocks/mockDiscord.ts | 48 +++++++++++-------- 8 files changed, 96 insertions(+), 111 deletions(-) create mode 100644 src/__mocks__/firebase/database.ts create mode 100644 src/__mocks__/helpers.ts diff --git a/src/__mocks__/firebase/database.ts b/src/__mocks__/firebase/database.ts new file mode 100644 index 0000000..a0aec95 --- /dev/null +++ b/src/__mocks__/firebase/database.ts @@ -0,0 +1,21 @@ +import { DatabaseReference } from "firebase/database"; + +const firebaseDatabase = jest.requireActual("firebase/database"); +const mockFirebaseDatabase = jest.createMockFromModule("firebase/database"); + +const mockSnapshot = { + exists: () => true, + val: () => ({ count: 2 }) +}; + +mockFirebaseDatabase.get = jest.fn().mockResolvedValue(mockSnapshot); + +mockFirebaseDatabase.child = jest.fn((_, path): DatabaseReference => { + return `servers/${path}` as unknown as DatabaseReference; +}); +mockFirebaseDatabase.ref = jest.fn().mockReturnValue('mock-ref'); +mockFirebaseDatabase.set = jest.fn().mockResolvedValue(undefined); +mockFirebaseDatabase.remove = jest.fn().mockResolvedValue(undefined); + + +module.exports = mockFirebaseDatabase; diff --git a/src/__mocks__/helpers.ts b/src/__mocks__/helpers.ts new file mode 100644 index 0000000..e6138c2 --- /dev/null +++ b/src/__mocks__/helpers.ts @@ -0,0 +1,7 @@ +const helpers = jest.requireActual("../helpers"); +const mockHelpers = jest.createMockFromModule("../helpers"); + +module.exports = { + ...helpers, + MINIMUM_MOOD_LIFESPAN: 0 +}; \ No newline at end of file diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 7b21d98..1ceaa6c 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,49 +1,16 @@ import * as discordJS from "discord.js"; +import * as firebase from "firebase/database"; import { MockDiscord } from "./testing/mocks/mockDiscord"; import { getTimestampFromSnowflake } from "./helpers"; jest.mock("discord.js"); - -type MockDatabase = { - get: jest.Mock; - child: jest.Mock; - ref: jest.Mock; - set: jest.Mock; - remove: jest.Mock; - getDatabase: jest.Mock; -} - -jest.mock('firebase/database', (): MockDatabase => { - const mockSnapshot = { - exists: () => true, - val: () => ({ count: 2 }) - }; - - const mockGet = jest.fn().mockResolvedValue(mockSnapshot); - const mockChild = jest.fn((_, path) => { - return `servers/${path}`; - }); - const mockRef = jest.fn().mockReturnValue('mock-ref'); - const mockSet = jest.fn().mockResolvedValue(undefined); - const mockRemove = jest.fn().mockResolvedValue(undefined); - - return { - get: mockGet, - child: mockChild, - ref: mockRef, - set: mockSet, - remove: mockRemove, - getDatabase: jest.fn().mockReturnValue({ - ref: mockRef - }) - }; -}); +jest.mock("firebase/database"); describe("Testing helper functions", () => { beforeAll(()=>{ process.env.APP_ID = "TEST APP ID"; process.env.DISCORD_TOKEN = "TEST TOKEN"; - jest.spyOn(console, "log").mockImplementation(() => {}); + // jest.spyOn(console, "log").mockImplementation(() => {}); }); describe("Testing getTimestampFromSnowflake", () => { diff --git a/src/helpers.ts b/src/helpers.ts index db59ede..be41881 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,6 @@ import { CacheType, ChatInputCommandInteraction, Client, FetchMembersOptions, Guild, Role, Snowflake} from "discord.js"; import db from './firebase'; // Import from your firebase.ts file -import { ref, set, get, child, remove } from "firebase/database"; +import { ref, set, get, child, remove } from "firebase/database"; // the minimum amount of time a mood can be in a server before it gets deleted automatically export const MINIMUM_MOOD_LIFESPAN: number = 0.5 * (1000 * 60); @@ -31,8 +31,9 @@ export async function addRoleToDatabase(guildId: string, role: Role): Promise { + return await setPromise.then((): string => { return "role successfully set" }).catch((): string => { return "something went wrong"; @@ -79,8 +80,10 @@ export async function removeRoleIfUnused(role: Role | null): Promise { if (timeDifference < MINIMUM_MOOD_LIFESPAN) { return "role is too young to be removed" } - + + return await role.guild.members.fetch().then((members): string => { + // console.log(members); if (members.some(member => member.roles.cache.some(memberRole => memberRole.id === role.id))) { return "role still in use" } diff --git a/src/index.test.ts b/src/index.test.ts index ceca8a4..542d1a1 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -7,7 +7,7 @@ describe("Testing slash commands", ()=>{ process.env.APP_ID = "TEST APP ID"; process.env.DISCORD_TOKEN = "TEST TOKEN"; - jest.spyOn(console, "log").mockImplementation(() => {}); + // jest.spyOn(console, "log").mockImplementation(() => {}); }); test("index.ts should run launchBot() from discordApp.ts", async ()=>{ diff --git a/src/interactions.test.ts b/src/interactions.test.ts index a6795ac..77e83fb 100644 --- a/src/interactions.test.ts +++ b/src/interactions.test.ts @@ -2,47 +2,19 @@ import * as discordJS from "discord.js"; import { embed, ping, tone, mood, clarify, requestAnonymousClarification } from "./interactions"; import { MockDiscord } from "./testing/mocks/mockDiscord"; import * as gptRequests from "./gptRequests"; +import * as firebase from "firebase/database"; +import * as helpers from "./helpers"; jest.mock("./gptRequests") jest.mock("discord.js"); - -type MockDatabase = { - get: jest.Mock; - child: jest.Mock; - ref: jest.Mock; - set: jest.Mock; - getDatabase: jest.Mock; -} - -jest.mock('firebase/database', (): MockDatabase => { - const mockSnapshot = { - exists: () => true, - val: () => ({ mood: "previous-mood" }) - }; - - // Mock functions that will be exported - const mockGet = jest.fn().mockResolvedValue(mockSnapshot); - const mockChild = jest.fn((_, path) => { - return `servers/${path}`; - }); - const mockRef = jest.fn().mockReturnValue('mock-ref'); - - return { - get: mockGet, - child: mockChild, - ref: mockRef, - set: jest.fn().mockResolvedValue(undefined), - getDatabase: jest.fn().mockReturnValue({ - ref: mockRef - }) - }; -}); +jest.mock("firebase/database"); +jest.mock("./helpers"); describe("Testing application commands", ()=>{ beforeAll(()=>{ process.env.APP_ID = "TEST APP ID"; process.env.DISCORD_TOKEN = "TEST TOKEN"; - jest.spyOn(console, "log").mockImplementation(() => {}); + // jest.spyOn(console, "log").mockImplementation(() => {}); }); describe("Testing ping command", () => { @@ -61,7 +33,7 @@ describe("Testing application commands", ()=>{ await ping(interaction); const endTime = jest.getRealSystemTime(); - expect(endTime-startTime).toBeGreaterThanOrEqual(1000); + expect(endTime-startTime).toBeGreaterThanOrEqual(999); expect(spyDeferReply).toHaveBeenCalled(); expect(spyEditReply).toHaveBeenCalledWith("pong!"); }); @@ -82,7 +54,7 @@ describe("Testing application commands", ()=>{ await ping(interaction).catch(spyReject); const endTime = jest.getRealSystemTime(); - expect(endTime-startTime).toBeGreaterThanOrEqual(1000); + expect(endTime-startTime).toBeGreaterThanOrEqual(999); expect(spyDeferReply).toHaveBeenCalled(); expect(spyEditReply).not.toHaveBeenCalled(); expect(spyReject).toHaveBeenCalled(); @@ -281,23 +253,26 @@ Here's a short list of tones: \`\` (***TODO***)`; get: jest.fn(() => 1234567890) }); - const { set: mockSet, get: mockGet } = require('firebase/database') as MockDatabase; - mockSet.mockReset(); - mockGet.mockReset(); + const mockSet = jest.spyOn(firebase, "set"); + const mockGet = jest.spyOn(firebase, "get"); + const setTimeout = jest.fn(); + mockGet.mockResolvedValue({ exists: () => true, - val: () => ({ mood: "angry" }) - }); + val: () => ({ mood: "angry-role-id" }) + } as unknown as firebase.DataSnapshot); + + jest.useFakeTimers(); await mood(interaction); - + jest.runAllTimers(); // Check that the array of calls includes our expected call expect(mockSet.mock.calls).toContainEqual([ "mock-ref", { - mood: "happy", + mood: "happy-role-id", timestamp: 1234567890 } ]); @@ -325,13 +300,13 @@ Here's a short list of tones: \`\` (***TODO***)`; get: jest.fn(() => 1234567890) }); - const { set: mockSet, get: mockGet } = require('firebase/database') as MockDatabase; - mockSet.mockReset(); - mockGet.mockReset(); + const mockSet = jest.spyOn(firebase, "set"); + const mockGet = jest.spyOn(firebase, "get"); + mockGet.mockResolvedValue({ exists: () => false, val: () => null - }); + } as unknown as firebase.DataSnapshot); await mood(interaction); @@ -339,7 +314,7 @@ Here's a short list of tones: \`\` (***TODO***)`; expect(mockSet.mock.calls).toContainEqual([ "mock-ref", { - mood: "excited", + mood: "excited-role-id", timestamp: 1234567890 } ]); @@ -365,9 +340,9 @@ Here's a short list of tones: \`\` (***TODO***)`; get: jest.fn(() => 1234567890) }); - const { set: mockSet, get: mockGet } = require('firebase/database') as MockDatabase; - mockSet.mockReset(); - mockGet.mockReset(); + const mockSet = jest.spyOn(firebase, "set"); + const mockGet = jest.spyOn(firebase, "get"); + mockGet.mockRejectedValue(error); const spyStdErr = jest.spyOn(console, "error").mockImplementation(() => {}); diff --git a/src/interactions.ts b/src/interactions.ts index 4b82a11..c11369c 100644 --- a/src/interactions.ts +++ b/src/interactions.ts @@ -155,13 +155,15 @@ export async function mood(interaction: ChatInputCommandInteraction): member.roles.remove(role); // wait a bit for the cache to update // maybe just clean roles after a certain interval of time eventually - setTimeout(() => removeRoleIfUnused(role), MINIMUM_MOOD_LIFESPAN); + setTimeout(() => { + removeRoleIfUnused(role); + }, MINIMUM_MOOD_LIFESPAN); } }).catch((error) => { console.error(error); }); } - + // update new role if (newRole) { member.roles.add(newRole); diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 64702e7..b067f6c 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -21,7 +21,8 @@ import { ChatInputCommandInteraction, InteractionReplyOptions, MessageCollector, - UserFlags + UserFlags, + GuildCreateOptions } from "discord.js"; type MockDiscordOptions = { @@ -98,35 +99,44 @@ export class MockDiscord { } as unknown as GuildMember; // Then create guild with reference to guildMember + let memberCache = new Collection([[this.user.id, this.guildMember]]); this.guild = { client: this.client, id: "guild-id", name: "mock guild", roles: { cache: this.roles, - fetch: jest.fn().mockResolvedValue({ - id: "role-id", - name: "role-name", - color: 0x000000 - }), - create: jest.fn().mockImplementation((options: any) => { - // console.log("create role called with options: " + options.name + " " + options.color); - const newRole = { - id: `${options.name}-role-id`, - name: options.name, - color: options.color || 0x000000 - }; - this.roles.set(newRole.id, newRole); - // console.log("found role:", JSON.stringify(this.roles.find(role => role.name === "happy"))); - return Promise.resolve(newRole); - }) + fetch: jest.fn(), + create: jest.fn() }, members: { - cache: new Map([[this.user.id, this.guildMember]]), - fetch: jest.fn().mockResolvedValue(this.guildMember) + cache: memberCache, + fetch: jest.fn(async (query) => { + if (query == this.user.id) { + return this.guildMember; + } else { + return memberCache; + } + }), + some: jest.fn().mockResolvedValue(true) } } as unknown as Guild; + this.guild.roles.fetch = jest.fn(async (id) => this.roles.get(id)) as jest.Mock + this.guild.roles.create = jest.fn().mockImplementation((options: any) => { + // console.log("create role called with options: " + options.name + " " + options.color); + const newRole = { + id: `${options.name}-role-id`, + name: options.name, + color: options.color || 0x000000, + guild: this.guild, + delete: jest.fn((_) => this.roles.delete(`${options.name}-role-id`,)) + }; + this.roles.set(newRole.id, newRole); + // console.log("found role:", JSON.stringify(this.roles.find(role => role.name === "happy"))); + return Promise.resolve(newRole); + }); + // Set guild reference in guildMember this.guildMember.guild = this.guild; From 417812e426af102f54cbb2dce7d6876be3c2d85c Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Fri, 21 Mar 2025 17:46:08 -0400 Subject: [PATCH 09/13] snowflake to timestamp tests written --- src/helpers.test.ts | 12 +++++++++++- src/helpers.ts | 2 +- src/interactions.test.ts | 20 ++++++++++---------- src/testing/mocks/mockDiscord.ts | 6 +++--- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 1ceaa6c..b369542 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -17,10 +17,20 @@ describe("Testing helper functions", () => { test("the snowflake '0' should return the Discord Epoch (1420070400000)", () => { const inputSnowflake = "0"; const outputTime = 1420070400000; + console.log(Number((0n >> 22n) + 1420070400000n)); + + const result = getTimestampFromSnowflake(inputSnowflake); + + expect(result).toBe(outputTime); + }); + + test("the snowflake '175928847299117063' should return 2016-04-30 11:18:25.796 UTC (1462015105796)", () => { + const inputSnowflake = "175928847299117063"; + const outputTime = 1462015105796; const result = getTimestampFromSnowflake(inputSnowflake); expect(result).toBe(outputTime); }); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts index be41881..8a77087 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -14,7 +14,7 @@ export const MINIMUM_MOOD_LIFESPAN: number = 0.5 * (1000 * 60); * @returns the timestamp of the snowflake */ export function getTimestampFromSnowflake(snowflake: Snowflake): number { - return (parseInt(snowflake) >> 22) + 1420070400000; + return Number((BigInt(snowflake) >> 22n) + 1420070400000n); } /** diff --git a/src/interactions.test.ts b/src/interactions.test.ts index 77e83fb..fd54bd1 100644 --- a/src/interactions.test.ts +++ b/src/interactions.test.ts @@ -229,19 +229,19 @@ Here's a short list of tones: \`\` (***TODO***)`; }); describe("Testing mood command", () => { - test("`mood` command correctly sets 'happy' mood in database and guild, and removes old mood 'angry'", async () => { + test("`mood` command correctly sets 'happy' mood in database and guild, and removes old mood 'mad'", async () => { const discord = new MockDiscord({ command: "/mood", commandOptions: { currentmood: "happy" } }); - expect(discord.getRoles().find(role => role.name === "angry")).toBeUndefined(); + expect(discord.getRoles().find(role => role.name === "mad")).toBeUndefined(); - discord.addRoleToGuild("angry", "000000"); + discord.addRoleToGuild("mad", "000000"); discord.addRoleToGuild("happy", "000000"); - discord.addRoleToMember("angry"); + discord.addRoleToMember("mad"); - expect(discord.getRoles().find(role => role.name === "angry")).toBeDefined(); + expect(discord.getRoles().find(role => role.name === "mad")).toBeDefined(); const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; @@ -259,7 +259,7 @@ Here's a short list of tones: \`\` (***TODO***)`; mockGet.mockResolvedValue({ exists: () => true, - val: () => ({ mood: "angry-role-id" }) + val: () => ({ mood: `${("mad").length}` }) } as unknown as firebase.DataSnapshot); jest.useFakeTimers(); @@ -272,14 +272,14 @@ Here's a short list of tones: \`\` (***TODO***)`; expect(mockSet.mock.calls).toContainEqual([ "mock-ref", { - mood: "happy-role-id", + mood: `${("happy").length}`, timestamp: 1234567890 } ]); - expect(discord.getRoles().find(role => role.name === "angry")).toBeDefined(); + expect(discord.getRoles().find(role => role.name === "mad")).toBeDefined(); expect(discord.getMemberRoles().find(role => role.name === "happy")).toBeDefined(); - expect(discord.getMemberRoles().find(role => role.name === "angry")).toBeUndefined(); + expect(discord.getMemberRoles().find(role => role.name === "mad")).toBeUndefined(); }); @@ -314,7 +314,7 @@ Here's a short list of tones: \`\` (***TODO***)`; expect(mockSet.mock.calls).toContainEqual([ "mock-ref", { - mood: "excited-role-id", + mood: `${("excited").length}`, timestamp: 1234567890 } ]); diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index b067f6c..892e141 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -71,7 +71,7 @@ export class MockDiscord { this.memberRoles.set(roleObj.id, roleObj) } else { const newRole = { - id: `${role}-role-id`, + id: `${(role).length}`, name: role, color: 0x000000 } @@ -126,11 +126,11 @@ export class MockDiscord { this.guild.roles.create = jest.fn().mockImplementation((options: any) => { // console.log("create role called with options: " + options.name + " " + options.color); const newRole = { - id: `${options.name}-role-id`, + id: `${(options.name as string).length}`, name: options.name, color: options.color || 0x000000, guild: this.guild, - delete: jest.fn((_) => this.roles.delete(`${options.name}-role-id`,)) + delete: jest.fn((_) => this.roles.delete(`${(options.name as string).length}`,)) }; this.roles.set(newRole.id, newRole); // console.log("found role:", JSON.stringify(this.roles.find(role => role.name === "happy"))); From 7083c2e501f15211626bdf1eb821d69e503352d7 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Sat, 22 Mar 2025 11:57:11 -0400 Subject: [PATCH 10/13] added tests for adding and removing roles --- src/__mocks__/firebase/database.ts | 2 +- src/helpers.test.ts | 65 +++++++++++++++++++++++++++++- src/helpers.ts | 19 ++------- src/testing/mocks/mockDiscord.ts | 8 ++-- 4 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/__mocks__/firebase/database.ts b/src/__mocks__/firebase/database.ts index a0aec95..3848459 100644 --- a/src/__mocks__/firebase/database.ts +++ b/src/__mocks__/firebase/database.ts @@ -11,7 +11,7 @@ const mockSnapshot = { mockFirebaseDatabase.get = jest.fn().mockResolvedValue(mockSnapshot); mockFirebaseDatabase.child = jest.fn((_, path): DatabaseReference => { - return `servers/${path}` as unknown as DatabaseReference; + return `${path}` as unknown as DatabaseReference; }); mockFirebaseDatabase.ref = jest.fn().mockReturnValue('mock-ref'); mockFirebaseDatabase.set = jest.fn().mockResolvedValue(undefined); diff --git a/src/helpers.test.ts b/src/helpers.test.ts index b369542..98060b8 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,7 +1,7 @@ import * as discordJS from "discord.js"; import * as firebase from "firebase/database"; import { MockDiscord } from "./testing/mocks/mockDiscord"; -import { getTimestampFromSnowflake } from "./helpers"; +import { addRoleToDatabase, getTimestampFromSnowflake, removeRoleFromDatabase } from "./helpers"; jest.mock("discord.js"); jest.mock("firebase/database"); @@ -17,7 +17,6 @@ describe("Testing helper functions", () => { test("the snowflake '0' should return the Discord Epoch (1420070400000)", () => { const inputSnowflake = "0"; const outputTime = 1420070400000; - console.log(Number((0n >> 22n) + 1420070400000n)); const result = getTimestampFromSnowflake(inputSnowflake); @@ -33,4 +32,66 @@ describe("Testing helper functions", () => { expect(result).toBe(outputTime); }); }); + + describe("Testing addRoleToDatabase", () => { + test("addRoleToDatabase should set servers/[guild id]/roles/[role name] to the role id", async () => { + const role = { name: "role-name", id: "role-id" } as unknown as discordJS.Role; + const guildId = "guild-id"; + + const mockSet = jest.spyOn(firebase, "set"); + + const result = await addRoleToDatabase(guildId, role); + + expect(mockSet).toHaveBeenCalled(); + expect(mockSet.mock.calls[0][0]).toBe(`servers/${guildId}/roles/${role.name}`); + expect(mockSet.mock.calls[0][1]).toBe(role.id); + + expect(result).toBe("role successfully set"); + }); + + test("addRoleToDatabase should return \"something went wrong\" if `set` fails", async () => { + const role = { name: "role-name", id: "role-id" } as unknown as discordJS.Role; + const guildId = "guild-id"; + + const mockSet = jest.spyOn(firebase, "set"); + mockSet.mockRejectedValueOnce(undefined); + + const result = await addRoleToDatabase(guildId, role); + + expect(mockSet).toHaveBeenCalled(); + + expect(result).toBe("something went wrong"); + }); + }); + + describe("Testing removeRoleFromDatabase", () => { + test("removeRoleFromDatabase should remove servers/[guild id]/roles/[role name] from the database", async () => { + const role = { name: "role-name", id: "role-id" } as unknown as discordJS.Role; + const guildId = "guild-id"; + + const mockRemove = jest.spyOn(firebase, "remove"); + + const result = await removeRoleFromDatabase(guildId, role); + + expect(mockRemove).toHaveBeenCalled(); + expect(mockRemove.mock.calls[0][0]).toBe(`servers/${guildId}/roles/${role.name}`); + + expect(result).toBe("role successfully removed"); + }); + + test("removeRoleFromDatabase should return \"something went wrong\" if `remove` fails", async () => { + const role = { name: "role-name", id: "role-id" } as unknown as discordJS.Role; + const guildId = "guild-id"; + + const mockRemove = jest.spyOn(firebase, "remove"); + mockRemove.mockRejectedValueOnce(undefined); + + const result = await removeRoleFromDatabase(guildId, role); + + expect(mockRemove).toHaveBeenCalled(); + + expect(result).toBe("something went wrong"); + }); + }); + }); \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts index 8a77087..89a32ed 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -25,15 +25,9 @@ export function getTimestampFromSnowflake(snowflake: Snowflake): number { * @returns a string containing the status of the operation */ export async function addRoleToDatabase(guildId: string, role: Role): Promise { - // if no role is given, exit - if (!role){ - return "Invalid role specified"; - } - - let rolesReference = ref(db, `servers/${guildId}/roles`); - let setPromise = set(child(rolesReference, role.name), role.id); + let rolesReference = ref(db); - return await setPromise.then((): string => { + return await set(child(rolesReference, `servers/${guildId}/roles/${role.name}`), role.id).then((): string => { return "role successfully set" }).catch((): string => { return "something went wrong"; @@ -48,14 +42,9 @@ export async function addRoleToDatabase(guildId: string, role: Role): Promise { - // if no role is given, exit - if (!role){ - return "Invalid role specified"; - } - - let rolesReference = ref(db, `servers/${guildId}/roles`); + let rolesReference = ref(db); - return await remove(child(rolesReference, role.name)).then((): string => { + return await remove(child(rolesReference, `servers/${guildId}/roles/${role.name}`)).then((): string => { return "role successfully removed" }).catch((): string => { return "something went wrong"; diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 892e141..6a4ef22 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -70,14 +70,14 @@ export class MockDiscord { if (roleObj) { this.memberRoles.set(roleObj.id, roleObj) } else { - const newRole = { - id: `${(role).length}`, + const newRole = { + id: `${(role).length}`, name: role, color: 0x000000 } this.guild.roles.create(newRole); this.memberRoles.set(role, newRole) - } + } } else { this.memberRoles.set(role.id, role); } @@ -125,7 +125,7 @@ export class MockDiscord { this.guild.roles.fetch = jest.fn(async (id) => this.roles.get(id)) as jest.Mock this.guild.roles.create = jest.fn().mockImplementation((options: any) => { // console.log("create role called with options: " + options.name + " " + options.color); - const newRole = { + const newRole = { id: `${(options.name as string).length}`, name: options.name, color: options.color || 0x000000, From 95bff2513b2e61dc1f9956b2cc4e63142161470b Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Sun, 23 Mar 2025 12:06:48 -0400 Subject: [PATCH 11/13] removeRoleIfUnused tested --- src/helpers.test.ts | 85 +++++++++++++++++++++++++++++++- src/helpers.ts | 12 +++++ src/interactions.test.ts | 21 ++++---- src/testing/mocks/mockDiscord.ts | 53 +------------------- 4 files changed, 108 insertions(+), 63 deletions(-) diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 98060b8..d7d82ac 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,7 +1,7 @@ import * as discordJS from "discord.js"; import * as firebase from "firebase/database"; import { MockDiscord } from "./testing/mocks/mockDiscord"; -import { addRoleToDatabase, getTimestampFromSnowflake, removeRoleFromDatabase } from "./helpers"; +import { addRoleToDatabase, getTimestampFromSnowflake, MINIMUM_MOOD_LIFESPAN, removeRoleFromDatabase, removeRoleIfUnused, timestampToSnowflake } from "./helpers"; jest.mock("discord.js"); jest.mock("firebase/database"); @@ -33,6 +33,26 @@ describe("Testing helper functions", () => { }); }); + describe("Testing timestampToSnowflake", () => { + test("the Discord Epoch (1420070400000) should return the snowflake '0'", () => { + const inputTime = 1420070400000; + const outputSnowflake = "0"; + + const result = timestampToSnowflake(inputTime); + + expect(result).toBe(outputSnowflake); + }); + + test("the time 2016-04-30 11:18:25.796 UTC (1462015105796) should return the snowflake '175928847298985984'", () => { + const inputTime = 1462015105796; + const outputSnowflake = "175928847298985984"; + + const result = timestampToSnowflake(inputTime); + + expect(result).toBe(outputSnowflake); + }); + }); + describe("Testing addRoleToDatabase", () => { test("addRoleToDatabase should set servers/[guild id]/roles/[role name] to the role id", async () => { const role = { name: "role-name", id: "role-id" } as unknown as discordJS.Role; @@ -94,4 +114,67 @@ describe("Testing helper functions", () => { }); }); + + describe("Testing removeRoleIfUnused", () => { + test("removeRoleIfUnused should return \"Invalid role specified\" if `role` is null", async () => { + const role = null; + const expectedResponse = "Invalid role specified"; + + const result = await removeRoleIfUnused(role); + + expect(result).toBe(expectedResponse); + }); + + test("removeRoleIfUnused should return \"role is too young to be removed\" if `role` has not existed for long enough", async () => { + const role = { name: "role-name", id: timestampToSnowflake(1462015105796 - (MINIMUM_MOOD_LIFESPAN - 1000)) } as unknown as discordJS.Role; + const expectedResponse = "role is too young to be removed"; + + const now = jest.spyOn(Date, "now"); + now.mockReturnValue(1462015105796); + + const result = await removeRoleIfUnused(role); + + expect(result).toBe(expectedResponse); + }); + + test("removeRoleIfUnused should return \"role removed\" if `role` has no users", async () => { + const discord = new MockDiscord({ command: "" }); + + discord.addRoleToGuild("role-name", "000000"); + + const role = discord.getRoles().find((storedRole) => storedRole.name === "role-name") as unknown as discordJS.Role; + const expectedResponse = "role removed"; + + const now = jest.spyOn(Date, "now"); + now.mockReturnValue(1420070400000 + (MINIMUM_MOOD_LIFESPAN + 1000)); + + const result = await removeRoleIfUnused(role); + + console.log(Date.now() - getTimestampFromSnowflake(role.id)); + + expect(result).toBe(expectedResponse); + }); + + test("removeRoleIfUnused should return \"role still in use\" if `role` is being used by another user", async () => { + const discord = new MockDiscord({ command: "" }); + + discord.addRoleToGuild("role-name", "000000"); + + const role = discord.getRoles().find((storedRole) => storedRole.name === "role-name") as unknown as discordJS.Role; + discord.addRoleToMember(role.id); + + const expectedResponse = "role still in use"; + + const now = jest.spyOn(Date, "now"); + now.mockReturnValue(1420070400000 + (MINIMUM_MOOD_LIFESPAN + 1000)); + + const result = await removeRoleIfUnused(role); + + console.log(Date.now() - getTimestampFromSnowflake(role.id)); + + expect(result).toBe(expectedResponse); + }); + }); + + }); \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts index 89a32ed..084249d 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -17,6 +17,18 @@ export function getTimestampFromSnowflake(snowflake: Snowflake): number { return Number((BigInt(snowflake) >> 22n) + 1420070400000n); } +/** + * fills the timestamp field of discord's Snowflake structure + * + * https://discord.com/developers/docs/reference#snowflakes + * + * @param timestamp the unix timestamp of the snowflake + * @returns the new snowflake + */ +export function timestampToSnowflake(timestamp: number): string { + return (BigInt(timestamp - 1420070400000) << 22n).toString(); +} + /** * adds a role to the database under a given guild * diff --git a/src/interactions.test.ts b/src/interactions.test.ts index fd54bd1..f6b5160 100644 --- a/src/interactions.test.ts +++ b/src/interactions.test.ts @@ -229,19 +229,19 @@ Here's a short list of tones: \`\` (***TODO***)`; }); describe("Testing mood command", () => { - test("`mood` command correctly sets 'happy' mood in database and guild, and removes old mood 'mad'", async () => { + test("`mood` command correctly sets 'happy' mood in database and guild, and removes old mood 'angry'", async () => { const discord = new MockDiscord({ command: "/mood", commandOptions: { currentmood: "happy" } }); - expect(discord.getRoles().find(role => role.name === "mad")).toBeUndefined(); + expect(discord.getRoles().find(role => role.name === "angry")).toBeUndefined(); - discord.addRoleToGuild("mad", "000000"); + discord.addRoleToGuild("angry", "000000"); discord.addRoleToGuild("happy", "000000"); - discord.addRoleToMember("mad"); + discord.addRoleToMember("angry"); - expect(discord.getRoles().find(role => role.name === "mad")).toBeDefined(); + expect(discord.getRoles().find(role => role.name === "angry")).toBeDefined(); const interaction = discord.getInteraction() as discordJS.ChatInputCommandInteraction; @@ -255,11 +255,10 @@ Here's a short list of tones: \`\` (***TODO***)`; const mockSet = jest.spyOn(firebase, "set"); const mockGet = jest.spyOn(firebase, "get"); - const setTimeout = jest.fn(); mockGet.mockResolvedValue({ exists: () => true, - val: () => ({ mood: `${("mad").length}` }) + val: () => ({ mood: `${helpers.timestampToSnowflake(1420070400000)}` }) } as unknown as firebase.DataSnapshot); jest.useFakeTimers(); @@ -272,14 +271,14 @@ Here's a short list of tones: \`\` (***TODO***)`; expect(mockSet.mock.calls).toContainEqual([ "mock-ref", { - mood: `${("happy").length}`, + mood: `${helpers.timestampToSnowflake(1420070400001)}`, timestamp: 1234567890 } ]); - expect(discord.getRoles().find(role => role.name === "mad")).toBeDefined(); + expect(discord.getRoles().find(role => role.name === "angry")).toBeDefined(); expect(discord.getMemberRoles().find(role => role.name === "happy")).toBeDefined(); - expect(discord.getMemberRoles().find(role => role.name === "mad")).toBeUndefined(); + expect(discord.getMemberRoles().find(role => role.name === "angry")).toBeUndefined(); }); @@ -314,7 +313,7 @@ Here's a short list of tones: \`\` (***TODO***)`; expect(mockSet.mock.calls).toContainEqual([ "mock-ref", { - mood: `${("excited").length}`, + mood: `${helpers.timestampToSnowflake(1420070400000)}`, timestamp: 1234567890 } ]); diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index 6a4ef22..c86c490 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -24,6 +24,7 @@ import { UserFlags, GuildCreateOptions } from "discord.js"; +import { timestampToSnowflake } from "../../helpers"; type MockDiscordOptions = { command: string, @@ -71,12 +72,10 @@ export class MockDiscord { this.memberRoles.set(roleObj.id, roleObj) } else { const newRole = { - id: `${(role).length}`, name: role, color: 0x000000 } this.guild.roles.create(newRole); - this.memberRoles.set(role, newRole) } } else { this.memberRoles.set(role.id, role); @@ -126,7 +125,7 @@ export class MockDiscord { this.guild.roles.create = jest.fn().mockImplementation((options: any) => { // console.log("create role called with options: " + options.name + " " + options.color); const newRole = { - id: `${(options.name as string).length}`, + id: timestampToSnowflake(1420070400000 + this.roles.size), name: options.name, color: options.color || 0x000000, guild: this.guild, @@ -197,54 +196,6 @@ export class MockDiscord { return mockUser; } - private createMockGuild(client: Client): Guild { - return { - client: client, - id: "guild-id", - name: "mock guild", - roles: { - cache: this.roles, - create: jest.fn().mockImplementation((options: any) => { - const newRole = { id: `${options.name}-role-id`, name: options.name }; - this.roles.set(options.name, newRole); - return Promise.resolve(newRole); - }) - }, - members: { - cache: new Map(), - fetch: jest.fn().mockImplementation((userId: string) => { - return Promise.resolve(this.guildMember); - }) - }, - } as unknown as Guild - } - - private createMockGuildMember(client: Client, user: User, guild: Guild): GuildMember { - return { - client: client, - deaf: false, - mute: false, - self_mute: false, - self_deaf: false, - session_id: "session-id", - channel_id: "channel-id", - nick: "nick", - user: user, - roles: { - cache: this.memberRoles, - add: jest.fn().mockImplementation((role) => { - this.memberRoles.set(role.id, role); - return Promise.resolve(this); - }), - remove: jest.fn().mockImplementation((role) => { - this.memberRoles.delete(role.id); - return Promise.resolve(this); - }) - }, - guild: guild - } as unknown as GuildMember - } - public createMockInteraction(command: string, mockGuild: any, mockMember: any, repliable: boolean = true, options: {} = {}): CommandInteraction { return { client: this.client, From 9696e7605493dfacc72dfb975758a87b7681a8d0 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Sun, 23 Mar 2025 12:26:12 -0400 Subject: [PATCH 12/13] permission denied test for cleanupMoods --- src/helpers.test.ts | 22 ++++++++++++++++------ src/helpers.ts | 10 ++++------ src/testing/mocks/mockDiscord.ts | 4 ++++ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/helpers.test.ts b/src/helpers.test.ts index d7d82ac..325e062 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -1,7 +1,7 @@ import * as discordJS from "discord.js"; import * as firebase from "firebase/database"; import { MockDiscord } from "./testing/mocks/mockDiscord"; -import { addRoleToDatabase, getTimestampFromSnowflake, MINIMUM_MOOD_LIFESPAN, removeRoleFromDatabase, removeRoleIfUnused, timestampToSnowflake } from "./helpers"; +import { addRoleToDatabase, cleanupMoods, getTimestampFromSnowflake, MINIMUM_MOOD_LIFESPAN, removeRoleFromDatabase, removeRoleIfUnused, timestampToSnowflake } from "./helpers"; jest.mock("discord.js"); jest.mock("firebase/database"); @@ -11,6 +11,7 @@ describe("Testing helper functions", () => { process.env.DISCORD_TOKEN = "TEST TOKEN"; // jest.spyOn(console, "log").mockImplementation(() => {}); + jest.spyOn(console, "error").mockImplementation(() => {}); }); describe("Testing getTimestampFromSnowflake", () => { @@ -150,8 +151,6 @@ describe("Testing helper functions", () => { const result = await removeRoleIfUnused(role); - console.log(Date.now() - getTimestampFromSnowflake(role.id)); - expect(result).toBe(expectedResponse); }); @@ -170,11 +169,22 @@ describe("Testing helper functions", () => { const result = await removeRoleIfUnused(role); - console.log(Date.now() - getTimestampFromSnowflake(role.id)); - expect(result).toBe(expectedResponse); }); }); - + describe("Testing cleanupMoods", () => { + test("cleanupMoods should return \"Bot does not have access to the specified guild\" if fetching the guild throws an error", async () => { + const discord = new MockDiscord({ command: "" }); + const guildId = "0"; + const expectedResponse = "Bot does not have access to the specified guild"; + + const mockFetch = jest.spyOn(discord.getUser().client.guilds, "fetch"); + mockFetch.mockRejectedValue("mock-err") + + const result = await cleanupMoods(discord.getUser().client, guildId); + + expect(result).toBe(expectedResponse); + }); + }); }); \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts index 084249d..6e5e5dc 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -103,10 +103,7 @@ export async function removeRoleIfUnused(role: Role | null): Promise { * @returns a string containing the status of the operation */ export async function cleanupMoods(client: Client, guildId: string): Promise { - let guild = await client.guilds.fetch(guildId); - // if no role is given, exit - if (guild){ - + return client.guilds.fetch(guildId).then(async (guild) => { let rolesReference = ref(db, `servers/${guildId}/roles`); let snapshot = await get(rolesReference); @@ -118,7 +115,8 @@ export async function cleanupMoods(client: Client, guildId: string): Promise { + console.error(error); return "Bot does not have access to the specified guild"; - } + }); } \ No newline at end of file diff --git a/src/testing/mocks/mockDiscord.ts b/src/testing/mocks/mockDiscord.ts index c86c490..1cfba56 100644 --- a/src/testing/mocks/mockDiscord.ts +++ b/src/testing/mocks/mockDiscord.ts @@ -158,6 +158,10 @@ export class MockDiscord { return this.user; } + public getClient(): User { + return this.user; + } + public getGuild(): Guild { return this.guild; } From cf3f65c1991d69a99bfc073e9014e0d76d8eb583 Mon Sep 17 00:00:00 2001 From: BoppleOpple Date: Sun, 23 Mar 2025 12:44:07 -0400 Subject: [PATCH 13/13] tests are bad, but theyre tests --- src/__mocks__/firebase/database.ts | 13 +++++++++++-- src/helpers.test.ts | 18 +++++++++++++++++- src/helpers.ts | 4 ++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/__mocks__/firebase/database.ts b/src/__mocks__/firebase/database.ts index 3848459..73ce908 100644 --- a/src/__mocks__/firebase/database.ts +++ b/src/__mocks__/firebase/database.ts @@ -1,11 +1,12 @@ -import { DatabaseReference } from "firebase/database"; +import { DatabaseReference, DataSnapshot } from "firebase/database"; const firebaseDatabase = jest.requireActual("firebase/database"); const mockFirebaseDatabase = jest.createMockFromModule("firebase/database"); const mockSnapshot = { exists: () => true, - val: () => ({ count: 2 }) + val: () => ({ count: 2 }), + forEach: () => true }; mockFirebaseDatabase.get = jest.fn().mockResolvedValue(mockSnapshot); @@ -17,5 +18,13 @@ mockFirebaseDatabase.ref = jest.fn().mockReturnValue('mock-ref'); mockFirebaseDatabase.set = jest.fn().mockResolvedValue(undefined); mockFirebaseDatabase.remove = jest.fn().mockResolvedValue(undefined); +class mockDataSnapshot { + forEach: jest.Mock; + constructor() { + this.forEach = jest.fn().mockReturnValue(true); + } +} + +mockFirebaseDatabase.DataSnapshot = mockDataSnapshot as unknown as typeof DataSnapshot; module.exports = mockFirebaseDatabase; diff --git a/src/helpers.test.ts b/src/helpers.test.ts index 325e062..d620830 100644 --- a/src/helpers.test.ts +++ b/src/helpers.test.ts @@ -11,7 +11,7 @@ describe("Testing helper functions", () => { process.env.DISCORD_TOKEN = "TEST TOKEN"; // jest.spyOn(console, "log").mockImplementation(() => {}); - jest.spyOn(console, "error").mockImplementation(() => {}); + // jest.spyOn(console, "error").mockImplementation(() => {}); }); describe("Testing getTimestampFromSnowflake", () => { @@ -186,5 +186,21 @@ describe("Testing helper functions", () => { expect(result).toBe(expectedResponse); }); + + test("cleanupMoods should return \"all unused roles removed\"", async () => { + const discord = new MockDiscord({ command: "" }); + const guildId = discord.getGuild().id; + const expectedResponse = "all unused roles removed"; + + const mockFetch = jest.spyOn(discord.getUser().client.guilds, "fetch"); + + // issue with ts and overloads + // @ts-ignore + mockFetch.mockResolvedValue(discord.getGuild()); + + const result = await cleanupMoods(discord.getUser().client, guildId); + + expect(result).toBe(expectedResponse); + }); }); }); \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts index 6e5e5dc..1c03278 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -104,9 +104,9 @@ export async function removeRoleIfUnused(role: Role | null): Promise { */ export async function cleanupMoods(client: Client, guildId: string): Promise { return client.guilds.fetch(guildId).then(async (guild) => { - let rolesReference = ref(db, `servers/${guildId}/roles`); + let rolesReference = ref(db); - let snapshot = await get(rolesReference); + let snapshot = await get(child(rolesReference, `servers/${guildId}/roles`)); snapshot.forEach((roleSnapshot) => { let roleSnowflake: Snowflake = roleSnapshot.val();