diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..dae5bfb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +.env +config.json +README.md diff --git a/.gitignore b/.gitignore index ceaea36..3da99eb 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dist .yarn/install-state.gz .pnp.* +# Runtime configuration (mounted as persistent data in production) +config.json + diff --git a/Dockerfile b/Dockerfile index 1612283..f2538af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,12 @@ -FROM node:22-alpine +FROM denoland/deno:alpine-2.9.4@sha256:13851184d6705150b8b230c5377f26c3bd182865d28700bb72bc0b2c271b504a WORKDIR /app -COPY package.json /app -COPY package-lock.json /app -COPY .env /app -COPY tsconfig.json /app -COPY src /app/src +COPY --chown=deno:deno deno.json deno.lock ./ +COPY --chown=deno:deno src ./src -RUN npm i +RUN deno cache --frozen src/index.ts -CMD ["npm", "run", "start"] \ No newline at end of file +USER deno + +CMD ["task", "start"] diff --git a/README.md b/README.md index 8dad0c5..eb1f4a1 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ # discord-reaction-award + +A Discord bot that republishes highly reacted messages into an award channel. Each user's first +reaction contributes 1 point; additional emoji from the same user have diminishing weight (1/3, 1/5, +...). + +## Configure Discord + +The bot needs the Guilds, Guild Messages, Guild Message Reactions, and privileged Message Content +gateway intents. It needs these channel permissions: + +- View Channel +- Read Message History +- Send Messages +- Embed Links + +Set TOKEN and CLIENT_ID in .env, then start the bot: + +```sh +cp default.env .env +deno task start +``` + +Global slash commands are registered at startup. Use `/config output` first, followed by the +optional channel list, emoji list, and score settings. Server owners and administrators can run +configuration commands. + +The runtime configuration is stored in `config.json`. Set `CONFIG_PATH` to move it elsewhere. Both +`.env` and `config.json` are intentionally excluded from Git and Docker build contexts. + +## Develop + +```sh +deno task check +docker build -t discord-reaction-award . +``` diff --git a/config.json b/config.json deleted file mode 100644 index 0637a08..0000000 --- a/config.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/default.env b/default.env index d5b953c..1e2d966 100644 --- a/default.env +++ b/default.env @@ -1,2 +1,2 @@ -TOKEN = 0 # Your bot token -CLIENT_ID = 0 # Your bot client ID \ No newline at end of file +TOKEN=your-bot-token +CLIENT_ID=your-application-id diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..0504443 --- /dev/null +++ b/deno.json @@ -0,0 +1,18 @@ +{ + "tasks": { + "check": "deno fmt --check && deno lint && deno check src/index.ts && deno test", + "start": "deno run --allow-env=TOKEN,CLIENT_ID,CONFIG_PATH --allow-net=discord.com,gateway.discord.gg --allow-read=./config.json,/data/config.json --allow-write=.,/data src/index.ts" + }, + "compilerOptions": { + "lib": ["deno.window"] + }, + "imports": { + "@std/assert": "jsr:@std/assert@1.0.19", + "discord.js": "npm:discord.js@14.27.0" + }, + "fmt": { + "lineWidth": 100, + "semiColons": true, + "singleQuote": false + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..268ecdb --- /dev/null +++ b/deno.lock @@ -0,0 +1,161 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@1.0.19": "1.0.19", + "jsr:@std/internal@^1.0.12": "1.0.14", + "npm:discord.js@14.27.0": "14.27.0" + }, + "jsr": { + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + } + }, + "npm": { + "@discordjs/builders@1.14.1": { + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "dependencies": [ + "@discordjs/formatters", + "@discordjs/util", + "@sapphire/shapeshift", + "discord-api-types", + "fast-deep-equal", + "ts-mixer", + "tslib" + ] + }, + "@discordjs/collection@1.5.3": { + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==" + }, + "@discordjs/collection@2.1.1": { + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==" + }, + "@discordjs/formatters@0.6.2": { + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "dependencies": [ + "discord-api-types" + ] + }, + "@discordjs/rest@2.6.3": { + "integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==", + "dependencies": [ + "@discordjs/collection@2.1.1", + "@discordjs/util", + "@sapphire/async-queue", + "@sapphire/snowflake", + "@vladfrangu/async_event_emitter", + "discord-api-types", + "magic-bytes.js", + "tslib", + "undici" + ] + }, + "@discordjs/util@1.2.0": { + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "dependencies": [ + "discord-api-types" + ] + }, + "@discordjs/ws@1.2.3": { + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "dependencies": [ + "@discordjs/collection@2.1.1", + "@discordjs/rest", + "@discordjs/util", + "@sapphire/async-queue", + "@types/ws", + "@vladfrangu/async_event_emitter", + "discord-api-types", + "tslib", + "ws" + ] + }, + "@sapphire/async-queue@1.5.5": { + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==" + }, + "@sapphire/shapeshift@4.0.0": { + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "dependencies": [ + "fast-deep-equal", + "lodash" + ] + }, + "@sapphire/snowflake@3.5.5": { + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==" + }, + "@types/node@26.2.0": { + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dependencies": [ + "undici-types" + ] + }, + "@types/ws@8.18.1": { + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": [ + "@types/node" + ] + }, + "@vladfrangu/async_event_emitter@2.4.7": { + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==" + }, + "discord-api-types@0.38.53": { + "integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==" + }, + "discord.js@14.27.0": { + "integrity": "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==", + "dependencies": [ + "@discordjs/builders", + "@discordjs/collection@1.5.3", + "@discordjs/formatters", + "@discordjs/rest", + "@discordjs/util", + "@discordjs/ws", + "@sapphire/snowflake", + "discord-api-types", + "fast-deep-equal", + "lodash.snakecase", + "magic-bytes.js", + "tslib", + "undici" + ] + }, + "fast-deep-equal@3.1.3": { + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "lodash.snakecase@4.1.1": { + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "lodash@4.18.1": { + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "magic-bytes.js@1.13.1": { + "integrity": "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==" + }, + "ts-mixer@6.0.4": { + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "undici-types@8.3.0": { + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + }, + "undici@6.28.0": { + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==" + }, + "ws@8.21.3": { + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@1.0.19", + "npm:discord.js@14.27.0" + ] + } +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a263b84..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1227 +0,0 @@ -{ - "name": "discord-reaction-award", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "discord.js": "^14.16.1", - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^22.5.4", - "tsx": "^4.19.0", - "typescript": "^5.5.4" - } - }, - "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==", - "dependencies": { - "@discordjs/formatters": "^0.5.0", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", - "engines": { - "node": ">=16.11.0" - } - }, - "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==", - "dependencies": { - "discord-api-types": "0.37.97" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "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==", - "dependencies": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "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==", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.3.0", - "@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", - "tslib": "^2.6.2", - "ws": "^8.16.0" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", - "engines": { - "node": ">=18" - }, - "funding": { - "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==" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "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==", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=v16" - } - }, - "node_modules/@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "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==" - }, - "node_modules/discord.js": { - "version": "14.16.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.16.1.tgz", - "integrity": "sha512-/diX4shp3q1F3EySGQbQl10el+KIpffLSOJdtM35gGV7zw2ED7rKbASKJT7UIR9L/lTd0KtNenZ/h739TN7diA==", - "dependencies": { - "@discordjs/builders": "^1.9.0", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.5.0", - "@discordjs/rest": "^2.4.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "1.1.1", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.97", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", - "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" - }, - "node_modules/magic-bytes.js": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", - "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==" - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "node_modules/tsx": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz", - "integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==", - "dev": true, - "dependencies": { - "esbuild": "~0.23.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", - "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==" - }, - "node_modules/ws": { - "version": "8.18.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - }, - "dependencies": { - "@discordjs/builders": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.9.0.tgz", - "integrity": "sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==", - "requires": { - "@discordjs/formatters": "^0.5.0", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "0.37.97", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - } - }, - "@discordjs/collection": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==" - }, - "@discordjs/formatters": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.5.0.tgz", - "integrity": "sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==", - "requires": { - "discord-api-types": "0.37.97" - } - }, - "@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==", - "requires": { - "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.1.1", - "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.3", - "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "0.37.97", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.19.8" - }, - "dependencies": { - "@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==" - } - } - }, - "@discordjs/util": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==" - }, - "@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==", - "requires": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.3.0", - "@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", - "tslib": "^2.6.2", - "ws": "^8.16.0" - }, - "dependencies": { - "@discordjs/collection": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==" - }, - "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==" - } - } - }, - "@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "dev": true, - "optional": true - }, - "@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "dev": true, - "optional": true - }, - "@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "dev": true, - "optional": true - }, - "@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "dev": true, - "optional": true - }, - "@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "dev": true, - "optional": true - }, - "@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "dev": true, - "optional": true - }, - "@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "dev": true, - "optional": true - }, - "@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "dev": true, - "optional": true - }, - "@esbuild/linux-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", - "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", - "dev": true, - "optional": true - }, - "@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "dev": true, - "optional": true - }, - "@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "dev": true, - "optional": true - }, - "@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "dev": true, - "optional": true - }, - "@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "dev": true, - "optional": true - }, - "@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "dev": true, - "optional": true - }, - "@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "dev": true, - "optional": true - }, - "@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==" - }, - "@sapphire/shapeshift": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", - "requires": { - "fast-deep-equal": "^3.1.3", - "lodash": "^4.17.21" - } - }, - "@sapphire/snowflake": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==" - }, - "@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", - "requires": { - "undici-types": "~6.19.2" - } - }, - "@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", - "requires": { - "@types/node": "*" - } - }, - "@vladfrangu/async_event_emitter": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==" - }, - "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==" - }, - "discord.js": { - "version": "14.16.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.16.1.tgz", - "integrity": "sha512-/diX4shp3q1F3EySGQbQl10el+KIpffLSOJdtM35gGV7zw2ED7rKbASKJT7UIR9L/lTd0KtNenZ/h739TN7diA==", - "requires": { - "@discordjs/builders": "^1.9.0", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.5.0", - "@discordjs/rest": "^2.4.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "1.1.1", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "0.37.97", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "tslib": "^2.6.3", - "undici": "6.19.8" - } - }, - "dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" - }, - "esbuild": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", - "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.23.1", - "@esbuild/android-arm": "0.23.1", - "@esbuild/android-arm64": "0.23.1", - "@esbuild/android-x64": "0.23.1", - "@esbuild/darwin-arm64": "0.23.1", - "@esbuild/darwin-x64": "0.23.1", - "@esbuild/freebsd-arm64": "0.23.1", - "@esbuild/freebsd-x64": "0.23.1", - "@esbuild/linux-arm": "0.23.1", - "@esbuild/linux-arm64": "0.23.1", - "@esbuild/linux-ia32": "0.23.1", - "@esbuild/linux-loong64": "0.23.1", - "@esbuild/linux-mips64el": "0.23.1", - "@esbuild/linux-ppc64": "0.23.1", - "@esbuild/linux-riscv64": "0.23.1", - "@esbuild/linux-s390x": "0.23.1", - "@esbuild/linux-x64": "0.23.1", - "@esbuild/netbsd-x64": "0.23.1", - "@esbuild/openbsd-arm64": "0.23.1", - "@esbuild/openbsd-x64": "0.23.1", - "@esbuild/sunos-x64": "0.23.1", - "@esbuild/win32-arm64": "0.23.1", - "@esbuild/win32-ia32": "0.23.1", - "@esbuild/win32-x64": "0.23.1" - } - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "optional": true - }, - "get-tsconfig": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.0.tgz", - "integrity": "sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw==", - "dev": true, - "requires": { - "resolve-pkg-maps": "^1.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" - }, - "magic-bytes.js": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", - "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==" - }, - "resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true - }, - "ts-mixer": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" - }, - "tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "tsx": { - "version": "4.19.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.0.tgz", - "integrity": "sha512-bV30kM7bsLZKZIOCHeMNVMJ32/LuJzLVajkQI/qf92J2Qr08ueLQvW00PUZGiuLPP760UINwupgUj8qrSCPUKg==", - "dev": true, - "requires": { - "esbuild": "~0.23.0", - "fsevents": "~2.3.3", - "get-tsconfig": "^4.7.5" - } - }, - "typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true - }, - "undici": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==" - }, - "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" - }, - "ws": { - "version": "8.18.0", - "requires": {} - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 0415edc..0000000 --- a/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "scripts": { - "start": "tsx src/index.ts" - }, - "type": "module", - "dependencies": { - "discord.js": "^14.16.1", - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^22.5.4", - "tsx": "^4.19.0", - "typescript": "^5.5.4" - } -} \ No newline at end of file diff --git a/src/award_message.ts b/src/award_message.ts index 56cce51..79d0f41 100644 --- a/src/award_message.ts +++ b/src/award_message.ts @@ -1,24 +1,27 @@ -import { EmbedBuilder, MessageReaction, PartialMessageReaction } from 'discord.js'; +import { EmbedBuilder, MessageReaction, PartialMessageReaction } from "discord.js"; + +export default function awardEmbed( + reaction: MessageReaction | PartialMessageReaction, + score: number, +): EmbedBuilder { + const message = reaction.message; + const reactionSummary = message.reactions.cache + .map((item) => `${item.emoji} ×${item.count}`) + .join(" · "); -export default function awardEmbed(reaction: MessageReaction | PartialMessageReaction, score: number) { - function formatTime(time: Date) { - const yyyy = time.getFullYear(); - const mm = time.getMonth().toString().padStart(2, '0'); - const dd = time.getDate().toString().padStart(2, '0'); - const hh = time.getHours().toString().padStart(2, '0'); - const MM = time.getMinutes().toString().padStart(2, '0'); - return `${yyyy}-${mm}-${dd} ${hh}:${MM}`; - } return new EmbedBuilder() .setColor(0x777777) .setAuthor({ - name: reaction.message.author?.displayName || "unknown", - iconURL: reaction.message.author?.avatarURL() || undefined, + name: message.author?.displayName || "Unknown user", + iconURL: message.author?.avatarURL() || undefined, }) - .setDescription(reaction.message.content || null) - .setThumbnail(reaction.message.attachments.filter((attachment) => !(!attachment.height && !attachment.width)).first()?.url || null) + .setDescription(message.content || null) + .setThumbnail( + message.attachments.find((attachment) => attachment.height || attachment.width)?.url ?? null, + ) .addFields({ - name: `${reaction.message.reactions.cache.map((reaction) => `${reaction.emoji} x${reaction.count},`).join(' ')} / ${score.toFixed(1)}`, - value: `${reaction.message.url} • ${formatTime(reaction.message.createdAt)}\n-# ${reaction.message.id}`, + name: `${reactionSummary} / ${score.toFixed(1)}`.slice(0, 256), + value: `${message.url} • `, }) -} \ No newline at end of file + .setFooter({ text: `Source message: ${message.id}` }); +} diff --git a/src/commands.ts b/src/commands.ts index 8c20d8a..fc5d1ae 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,83 +1,96 @@ -import { SlashCommandBuilder, SlashCommandSubcommandBuilder } from '@discordjs/builders'; +import { ChannelType, SlashCommandBuilder, SlashCommandSubcommandBuilder } from "discord.js"; const output = new SlashCommandSubcommandBuilder() - .setName('output') - .setDescription('Set the output channel') - .addChannelOption(option => - option.setName('channel') - .setDescription('The channel to send messages to') - .setRequired(true)); -const channels = new SlashCommandSubcommandBuilder() - .setName('channels') - .setDescription('Configure channel list') - .addStringOption(option => - option.setName('add_remove') - .setDescription('add_remove') + .setName("output") + .setDescription("Set the output channel") + .addChannelOption((option) => + option.setName("channel") + .setDescription("The channel to send messages to") + .addChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement) .setRequired(true) - .addChoices( - { name: 'Add', value: 'add' }, - { name: 'Remove', value: 'remove' } - )) - .addChannelOption(option => - option.setName('channel') - .setDescription('The channel to list') - .setRequired(true)); -const channels_type = new SlashCommandSubcommandBuilder() - .setName('channels_type') - .setDescription('Set the channel list type') - .addStringOption(option => - option.setName('channels_type') - .setDescription('include or exclude') - .setRequired(true) - .addChoices( - { name: 'Include', value: 'include' }, - { name: 'Exclude', value: 'exclude' } - )); -const emojis = new SlashCommandSubcommandBuilder() - .setName('emojis') - .setDescription('Configure emoji list') - .addStringOption(option => - option.setName('add_remove') - .setDescription('add_remove') - .setRequired(true) - .addChoices( - { name: 'Add', value: 'add' }, - { name: 'Remove', value: 'remove' } - )) - .addStringOption(option => - option.setName('emoji') - .setDescription('The emoji to list') - .setRequired(true)); -const emojis_type = new SlashCommandSubcommandBuilder() - .setName('emojis_type') - .setDescription('Set the emoji list type') - .addStringOption(option => - option.setName('emojis_type') - .setDescription('include or exclude') - .setRequired(true) - .addChoices( - { name: 'Include', value: 'include' }, - { name: 'Exclude', value: 'exclude' } - )); -const score = new SlashCommandSubcommandBuilder() - .setName('score') - .setDescription('Set the number of scores to trigger') - .addIntegerOption(option => - option.setName('score') - .setDescription('The number of scores to trigger') - .setRequired(true)); + ); +const channels = new SlashCommandSubcommandBuilder() + .setName("channels") + .setDescription("Configure the channel list") + .addStringOption((option) => + option.setName("action") + .setDescription("Add or remove the channel") + .setRequired(true) + .addChoices( + { name: "Add", value: "add" }, + { name: "Remove", value: "remove" }, + ) + ) + .addChannelOption((option) => + option.setName("channel") + .setDescription("The channel to add or remove") + .setRequired(true) + ); + +const channelsType = new SlashCommandSubcommandBuilder() + .setName("channels_type") + .setDescription("Set the channel list type") + .addStringOption((option) => + option.setName("type") + .setDescription("Use only listed channels, or ignore listed channels") + .setRequired(true) + .addChoices( + { name: "Include", value: "include" }, + { name: "Exclude", value: "exclude" }, + ) + ); + +const emojis = new SlashCommandSubcommandBuilder() + .setName("emojis") + .setDescription("Configure the emoji list") + .addStringOption((option) => + option.setName("action") + .setDescription("Add or remove the emoji") + .setRequired(true) + .addChoices( + { name: "Add", value: "add" }, + { name: "Remove", value: "remove" }, + ) + ) + .addStringOption((option) => + option.setName("emoji") + .setDescription("Unicode emoji or custom emoji") + .setRequired(true) + ); + +const emojisType = new SlashCommandSubcommandBuilder() + .setName("emojis_type") + .setDescription("Set the emoji list type") + .addStringOption((option) => + option.setName("type") + .setDescription("Use only listed emoji, or ignore listed emoji") + .setRequired(true) + .addChoices( + { name: "Include", value: "include" }, + { name: "Exclude", value: "exclude" }, + ) + ); + +const score = new SlashCommandSubcommandBuilder() + .setName("score") + .setDescription("Set the score needed to award a message") + .addIntegerOption((option) => + option.setName("score") + .setDescription("The score needed to award a message") + .setMinValue(1) + .setRequired(true) + ); const config = new SlashCommandBuilder() - .setName('config') - .setDescription('Configure the bot') + .setName("config") + .setDescription("Configure the bot") + .setDMPermission(false) .addSubcommand(output) .addSubcommand(channels) - .addSubcommand(channels_type) + .addSubcommand(channelsType) .addSubcommand(emojis) - .addSubcommand(emojis_type) + .addSubcommand(emojisType) .addSubcommand(score); -const commands = [config]; -export default commands; - +export default [config]; diff --git a/src/config.ts b/src/config.ts index 18c2834..b4f5173 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,21 +1,68 @@ -import fs from 'fs'; +import { createEntry, Entry, Type } from "./entry.ts"; -import { Entry } from './entry.ts'; +const configPath = Deno.env.get("CONFIG_PATH") ?? "./config.json"; +let pendingWrite = Promise.resolve(); -export async function writeConfig(entries: Entry[]) { - const data = JSON.stringify(entries, null, 2); - fs.writeFile('./config.json', data, (err) => { - if (err) { - console.error(err); - return; - } - }); +export function writeConfig(entries: Entry[]): Promise { + const data = `${JSON.stringify(entries, null, 2)}\n`; + pendingWrite = pendingWrite + .catch(() => undefined) + .then(async () => { + const temporaryPath = `${configPath}.tmp`; + await Deno.writeTextFile(temporaryPath, data); + await Deno.chmod(temporaryPath, 0o600); + await Deno.rename(temporaryPath, configPath); + }); + return pendingWrite; } export function readConfig(): Entry[] { - if (!fs.existsSync('./config.json')) { - return []; + let data: string; + try { + data = Deno.readTextFileSync(configPath); + } catch (error) { + if (error instanceof Deno.errors.NotFound) return []; + throw error; } - const data = fs.readFileSync('./config.json', 'utf8'); - return JSON.parse(data); -} \ No newline at end of file + + const parsed: unknown = JSON.parse(data); + if (!Array.isArray(parsed)) throw new Error("config must contain an array"); + return parsed.map((value, index) => parseEntry(value, index)); +} + +function parseEntry(value: unknown, index: number): Entry { + if (!value || typeof value !== "object") { + throw new Error(`config entry ${index} is invalid`); + } + + const raw = value as Record; + if (typeof raw.server !== "string" || raw.server.length === 0) { + throw new Error(`config entry ${index} has no server ID`); + } + + const entry = createEntry(raw.server); + if (typeof raw.output_channel === "string") entry.output_channel = raw.output_channel; + if (raw.channel_type === Type.INCLUDE || raw.channel_type === Type.EXCLUDE) { + entry.channel_type = raw.channel_type; + } + if (Array.isArray(raw.channels)) entry.channels = uniqueStrings(raw.channels); + if (raw.emoji_type === Type.INCLUDE || raw.emoji_type === Type.EXCLUDE) { + entry.emoji_type = raw.emoji_type; + } + if (Array.isArray(raw.emojis)) entry.emojis = uniqueStrings(raw.emojis); + if (typeof raw.score === "number" && Number.isFinite(raw.score) && raw.score > 0) { + entry.score = raw.score; + } + if (raw.awards && typeof raw.awards === "object" && !Array.isArray(raw.awards)) { + entry.awards = Object.fromEntries( + Object.entries(raw.awards).filter( + (item): item is [string, string] => typeof item[1] === "string", + ), + ); + } + return entry; +} + +function uniqueStrings(values: unknown[]): string[] { + return [...new Set(values.filter((item): item is string => typeof item === "string"))]; +} diff --git a/src/entry.ts b/src/entry.ts index 7edf8b2..52c6973 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -1,18 +1,28 @@ export enum Type { INCLUDE = "include", - EXCLUDE = "exclude" + EXCLUDE = "exclude", } -export class Entry { +export interface Entry { server: string; - output_channel: string = ""; - output_hook: string = ""; - channel_type: Type = Type.EXCLUDE; - channels: string[] = []; - emoji_type: Type = Type.EXCLUDE; - emojis: string[] = []; - score: number = 5; - constructor(server: string) { - this.server = server; - } -} \ No newline at end of file + output_channel: string; + channel_type: Type; + channels: string[]; + emoji_type: Type; + emojis: string[]; + score: number; + awards: Record; +} + +export function createEntry(server: string): Entry { + return { + server, + output_channel: "", + channel_type: Type.EXCLUDE, + channels: [], + emoji_type: Type.EXCLUDE, + emojis: [], + score: 5, + awards: {}, + }; +} diff --git a/src/index.ts b/src/index.ts index ebb69cd..e821005 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,38 @@ -import "dotenv/config"; -const TOKEN = process.env.TOKEN; -const CLIENT_ID = process.env.CLIENT_ID; -if (!TOKEN || !CLIENT_ID) { - console.error(`No ${TOKEN ? "CLIENT_ID" : "TOKEN"} provided`); - process.exit(); -} -import commands from "./commands.ts"; - import { + BaseGuildTextChannel, + ChatInputCommandInteraction, + Client, + Events, + GatewayIntentBits, + Message, + MessageReaction, + PartialMessage, + PartialMessageReaction, + Partials, + PermissionFlagsBits, REST, Routes, - Client, - Partials, - GatewayIntentBits, - Events, - TextChannel, - Message, - PartialMessage, } from "discord.js"; +import awardEmbed from "./award_message.ts"; +import commands from "./commands.ts"; +import { readConfig, writeConfig } from "./config.ts"; +import { createEntry, Entry, Type } from "./entry.ts"; +import { totalScore } from "./scoring.ts"; -const rest = new REST({ version: "10" }).setToken(TOKEN); -try { - console.log("refreshing slash commands..."); - await rest.put(Routes.applicationCommands(CLIENT_ID), { body: commands }); - console.log("OK"); -} catch (error) { - console.error(error); +const TOKEN = Deno.env.get("TOKEN"); +const CLIENT_ID = Deno.env.get("CLIENT_ID"); +if (!TOKEN || !CLIENT_ID) { + console.error(`Missing required environment variable: ${TOKEN ? "CLIENT_ID" : "TOKEN"}`); + Deno.exit(1); } -import { writeConfig, readConfig } from "./config.ts"; +const rest = new REST({ version: "10" }).setToken(TOKEN); +console.log("Refreshing slash commands..."); +await rest.put(Routes.applicationCommands(CLIENT_ID), { + body: commands.map((command) => command.toJSON()), +}); +console.log("Slash commands refreshed."); + const client = new Client({ intents: [ GatewayIntentBits.Guilds, @@ -39,310 +43,229 @@ const client = new Client({ partials: [Partials.Message, Partials.Channel, Partials.Reaction], }); -client.on("ready", () => { - if (!client.user) { - console.error("Failed to login"); - process.exit(); - } +const entries = readConfig(); +const updateQueues = new Map>(); - console.log(`Logged in as ${client.user.tag}!`); - client.user.setActivity("👑let's award funny messages!"); -}); - -import { Entry, Type } from "./entry.ts"; -const entries: Entry[] = readConfig(); -writeConfig(entries); - -client.on("interactionCreate", async (interaction) => { - if (interaction.isChatInputCommand()) { - if (interaction.commandName === "config") { - if (interaction.user.id !== interaction.guild?.ownerId) { - await interaction.reply({ - content: "Only the server owner can configure the bot", - ephemeral: true, - }); - return; - } - const subcommand = interaction.options.getSubcommand(true); - if (subcommand === "output") { - const channel = interaction.options.getChannel("channel", true); - try { - await interaction.deferReply({ ephemeral: true }); - let entry = entries.find( - (entry) => entry.server === interaction.guildId - ); - if (!entry) { - entry = new Entry(interaction.guildId!); - entries.push(entry); - } - entry.output_channel = channel.id; - writeConfig(entries); - } catch (error) { - console.error(error); - } finally { - await interaction.followUp({ - content: `Set output to <#${channel.id}>`, - }); - } - } else if (subcommand === "channels") { - const channel = interaction.options.getChannel("channel", true); - const mode = interaction.options.getString("add_remove", true); - try { - await interaction.deferReply({ ephemeral: true }); - let entry = entries.find( - (entry) => entry.server === interaction.guildId - ); - if (!entry) { - entry = new Entry(interaction.guildId!); - entries.push(entry); - } - if (mode === "add") entry.channels.push(channel.id); - else entry.channels = entry.channels.filter((c) => c !== channel.id); - writeConfig(entries); - } catch (error) { - console.error(error); - } finally { - await interaction.followUp({ - content: `Set to watch for reactions in <#${channel.id}>`, - }); - } - } else if (subcommand === "channels_type") { - const channel_type = interaction.options.getString( - "channels_type", - true - ); - try { - await interaction.deferReply({ ephemeral: true }); - - let entry = entries.find( - (entry) => entry.server === interaction.guildId - ); - if (!entry) { - entry = new Entry(interaction.guildId!); - entries.push(entry); - } - entry.channel_type = channel_type as Type; - writeConfig(entries); - } catch (error) { - console.error(error); - } finally { - await interaction.followUp({ - content: `channel list type set to ${channel_type}`, - ephemeral: true, - }); - } - } else if (subcommand === "emojis") { - const emoji = interaction.options.getString("emoji", true); - const mode = interaction.options.getString("add_remove", true); - try { - await interaction.deferReply({ ephemeral: true }); - let entry = entries.find( - (entry) => entry.server === interaction.guildId - ); - if (!entry) { - entry = new Entry(interaction.guildId!); - entries.push(entry); - } - if (mode === "add") entry.emojis.push(emoji); - else entry.emojis = entry.emojis.filter((e) => e !== emoji); - await writeConfig(entries); - } catch (error) { - console.error(error); - } finally { - await interaction.followUp({ - content: `Set to watch for ${emoji}`, - ephemeral: true, - }); - } - } else if (subcommand === "emojis_type") { - const emoji_type = interaction.options.getString("emojis_type", true); - try { - await interaction.deferReply({ ephemeral: true }); - let entry = entries.find( - (entry) => entry.server === interaction.guildId - ); - if (!entry) { - entry = new Entry(interaction.guildId!); - entries.push(entry); - } - entry.emoji_type = emoji_type as Type; - writeConfig(entries); - } catch (error) { - console.error(error); - } finally { - await interaction.followUp({ - content: `emoji list type set to ${emoji_type}`, - ephemeral: true, - }); - } - } else if (subcommand === "score") { - const score = interaction.options.getInteger("score", true); - try { - await interaction.deferReply({ ephemeral: true }); - let entry = entries.find( - (entry) => entry.server === interaction.guildId - ); - if (!entry) { - entry = new Entry(interaction.guildId!); - entries.push(entry); - } - entry.score = score; - await writeConfig(entries); - } catch (error) { - console.error(error); - } finally { - await interaction.followUp({ - content: `The score needed to trigger is now ${score}`, - ephemeral: true, - }); - } - } - } - } -}); - -import awardEmbed from "./award_message.ts"; -client.on(Events.MessageReactionAdd, async (reaction, user) => { - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - console.error("Something went wrong when fetching the message:", error); - return; - } - } - if (!reaction.message.guild) return; - - const entry = entries.find( - (entry) => entry.server === reaction.message.guildId +client.once(Events.ClientReady, (readyClient) => { + console.log( + `Logged in as ${readyClient.user.tag}; serving ${readyClient.guilds.cache.size} guild(s).`, ); - if (entry === undefined) return; - if ( - (entry.channel_type === Type.EXCLUDE) === - entry.channels.includes(reaction.message.channelId) - ) - return; - if ( - (entry.emoji_type === Type.EXCLUDE) === - entry.emojis.includes(`${reaction.emoji.id || reaction.emoji.name}`) - ) - return; + readyClient.user.setActivity("👑 awarding funny messages"); +}); - console.log(`reaction added: ${reaction.emoji.id || reaction.emoji.name}`); - const totalScore = await getMessageScore(entry, reaction.message); +client.on(Events.InteractionCreate, async (interaction) => { + if (!interaction.isChatInputCommand() || interaction.commandName !== "config") return; - if (totalScore >= entry.score) { - const channel = reaction.message.guild.channels.cache.get( - entry.output_channel - ) as TextChannel; - channel.messages.fetch({ limit: 10 }).then((messages) => { - messages.find((message) => { - if (message.embeds[0]?.fields[0].value.includes(reaction.message.id)) { - message.delete(); - console.log("old message deleted."); - return true; - } - return false; - }); - channel.send({ - embeds: [awardEmbed(reaction, totalScore)], - }); - console.log("message sent."); - }); + try { + await configure(interaction); + } catch (error) { + console.error("Configuration command failed:", error); + const content = "Configuration failed. Check the bot logs and try again."; + if (interaction.deferred || interaction.replied) { + await interaction.editReply({ content }); + } else { + await interaction.reply({ content, ephemeral: true }); + } } }); +client.on(Events.MessageReactionAdd, async (reaction, user) => { + if (!user.bot) await queueAwardUpdate(reaction); +}); + client.on(Events.MessageReactionRemove, async (reaction, user) => { - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - console.error("Something went wrong when fetching the message:", error); - return; - } - } - if (!reaction.message.guild) return; - - const entry = entries.find( - (entry) => entry.server === reaction.message.guildId - ); - if (entry === undefined) return; - if ( - (entry.channel_type === Type.EXCLUDE) === - entry.channels.includes(reaction.message.channelId) - ) - return; - if ( - (entry.emoji_type === Type.EXCLUDE) === - entry.emojis.includes(`${reaction.emoji.id || reaction.emoji.name}`) - ) - return; - - console.log(`reaction removed: ${reaction.emoji.id || reaction.emoji.name}`); - const totalScore = await getMessageScore(entry, reaction.message); - - const channel = reaction.message.guild.channels.cache.get( - entry.output_channel - ) as TextChannel; - channel.messages.fetch({ limit: 10 }).then((messages) => { - messages.find((message) => { - if (message.embeds[0]?.fields[0].value.includes(reaction.message.id)) { - message.delete(); - console.log("old message deleted."); - if (totalScore >= entry.score) { - channel.send({ - embeds: [awardEmbed(reaction, totalScore)], - }); - console.log("message sent."); - } - return true; - } - return false; - }); - }); + if (!user.bot) await queueAwardUpdate(reaction); }); -client.login(TOKEN); +client.on(Events.Error, (error) => console.error("Discord client error:", error)); +client.on(Events.Warn, (message) => console.warn("Discord client warning:", message)); + +await client.login(TOKEN); + +async function configure(interaction: ChatInputCommandInteraction): Promise { + if (!interaction.inCachedGuild()) { + await interaction.reply({ content: "Run this command in a server.", ephemeral: true }); + return; + } + + if ( + interaction.user.id !== interaction.guild.ownerId && + !interaction.memberPermissions.has(PermissionFlagsBits.Administrator) + ) { + await interaction.reply({ + content: "Only the server owner or an administrator can configure the bot.", + ephemeral: true, + }); + return; + } + + await interaction.deferReply({ ephemeral: true }); + let entry = entries.find((item) => item.server === interaction.guildId); + if (!entry) { + entry = createEntry(interaction.guildId); + entries.push(entry); + } + + const subcommand = interaction.options.getSubcommand(true); + let content: string; + switch (subcommand) { + case "output": { + const channel = interaction.options.getChannel("channel", true); + if (entry.output_channel !== channel.id) entry.awards = {}; + entry.output_channel = channel.id; + content = `Award messages will be sent to <#${channel.id}>.`; + break; + } + case "channels": { + const channel = interaction.options.getChannel("channel", true); + const action = interaction.options.getString("action", true); + entry.channels = updateList(entry.channels, channel.id, action); + content = `${action === "add" ? "Added" : "Removed"} <#${channel.id}> ` + + `${action === "add" ? "to" : "from"} the channel list.`; + break; + } + case "channels_type": { + entry.channel_type = interaction.options.getString("type", true) as Type; + content = `Channel list mode is now ${entry.channel_type}.`; + break; + } + case "emojis": { + const emojiInput = interaction.options.getString("emoji", true); + const emoji = normalizeEmoji(emojiInput); + const action = interaction.options.getString("action", true); + entry.emojis = updateList(entry.emojis, emoji, action); + content = `${action === "add" ? "Added" : "Removed"} ${emojiInput} ` + + `${action === "add" ? "to" : "from"} the emoji list.`; + break; + } + case "emojis_type": { + entry.emoji_type = interaction.options.getString("type", true) as Type; + content = `Emoji list mode is now ${entry.emoji_type}.`; + break; + } + case "score": { + entry.score = interaction.options.getInteger("score", true); + content = `Messages are now awarded at a score of ${entry.score}.`; + break; + } + default: + throw new Error(`Unknown subcommand: ${subcommand}`); + } + + await writeConfig(entries); + await interaction.editReply({ content }); +} + +function updateList(values: string[], value: string, action: string): string[] { + if (action === "add") return [...new Set([...values, value])]; + return values.filter((item) => item !== value); +} + +function normalizeEmoji(value: string): string { + return value.match(/^]+:(\d+)>$/)?.[1] ?? value; +} + +function queueAwardUpdate( + reaction: MessageReaction | PartialMessageReaction, +): Promise { + const messageId = reaction.message.id; + const previous = updateQueues.get(messageId) ?? Promise.resolve(); + const current = previous + .catch(() => undefined) + .then(() => updateAward(reaction)) + .catch((error) => console.error(`Failed to update award for message ${messageId}:`, error)) + .finally(() => { + if (updateQueues.get(messageId) === current) updateQueues.delete(messageId); + }); + updateQueues.set(messageId, current); + return current; +} + +async function updateAward( + reaction: MessageReaction | PartialMessageReaction, +): Promise { + if (reaction.partial) await reaction.fetch(); + if (reaction.message.partial) await reaction.message.fetch(); + + const message = reaction.message; + const entry = watchedEntry(message); + if (!message.guild || !entry) return; + + const outputChannel = await message.guild.channels.fetch(entry.output_channel); + if (!(outputChannel instanceof BaseGuildTextChannel)) { + console.warn( + `Configured output channel ${entry.output_channel} is missing or cannot contain messages.`, + ); + return; + } + + const score = await getMessageScore(entry, message); + const mappedAwardId = entry.awards[message.id]; + let oldAward = mappedAwardId + ? await outputChannel.messages.fetch(mappedAwardId).catch(() => undefined) + : undefined; + + if (!oldAward) { + const recent = await outputChannel.messages.fetch({ limit: 100 }); + oldAward = recent.find((item) => + item.author.id === client.user?.id && + item.embeds.some((embed) => + embed.footer?.text === `Source message: ${message.id}` || + embed.fields.some((field) => field.value.includes(message.id)) + ) + ); + } + + if (score < entry.score) { + if (oldAward) await oldAward.delete(); + if (mappedAwardId) { + delete entry.awards[message.id]; + await writeConfig(entries); + } + return; + } + + const payload = { embeds: [awardEmbed(reaction, score)] }; + if (oldAward) { + await oldAward.edit(payload); + if (entry.awards[message.id] !== oldAward.id) { + entry.awards[message.id] = oldAward.id; + await writeConfig(entries); + } + } else { + const award = await outputChannel.send(payload); + entry.awards[message.id] = award.id; + await writeConfig(entries); + } +} + +function watchedEntry(message: Message | PartialMessage): Entry | undefined { + const entry = entries.find((item) => item.server === message.guildId); + if (!entry?.output_channel) return undefined; + if (message.channelId === entry.output_channel) return undefined; + if (!listAllows(entry.channel_type, entry.channels, message.channelId)) return undefined; + return entry; +} + +function listAllows(type: Type, values: string[], value: string): boolean { + return type === Type.INCLUDE ? values.includes(value) : !values.includes(value); +} async function getMessageScore( entry: Entry, - message: Message | PartialMessage + message: Message | PartialMessage, ): Promise { - /* Message Score Calculation * - * Per user: 1 / (-1 + 2x) */ + const reactionCounts = new Map(); + for (const item of message.reactions.cache.values()) { + const emoji = item.emoji.id ?? item.emoji.name ?? ""; + if (!listAllows(entry.emoji_type, entry.emojis, emoji)) continue; - const allReactions: Map = new Map(); - const reactions = message.reactions.cache; - for (const r of reactions.values()) { - if ( - (entry.emoji_type === Type.EXCLUDE) === - entry.emojis.includes(`${r.emoji.id || r.emoji.name}`) - ) - continue; - const users = await r.users.fetch(); + const users = await item.users.fetch(); for (const user of users.values()) { - if (user.bot) continue; - const score = allReactions.get(user.id) || 0; - allReactions.set(user.id, score + 1 / (-1 + 2 * (score + 1))); + if (!user.bot) { + reactionCounts.set(user.id, (reactionCounts.get(user.id) ?? 0) + 1); + } } } - - let totalScore = 0; - allReactions.forEach((value) => { - totalScore += value; - }); - - // "message" by "user" - // id : score - // ---------------- - // Total: 1.5 - - console.log(`"${message.id}" by "${message.author?.username}"`); - allReactions.forEach((value, key) => { - console.log(`${key} : ${value}`); - }); - console.log(`----------------`); - console.log(`Total: ${totalScore}`); - - return totalScore; + return totalScore(reactionCounts.values()); } diff --git a/src/scoring.ts b/src/scoring.ts new file mode 100644 index 0000000..d7dbe5a --- /dev/null +++ b/src/scoring.ts @@ -0,0 +1,24 @@ +export function reactionWeight(reactionIndex: number): number { + if (!Number.isInteger(reactionIndex) || reactionIndex < 1) { + throw new RangeError("reaction index must be a positive integer"); + } + return 1 / (2 * reactionIndex - 1); +} + +export function scoreForUser(reactionCount: number): number { + if (!Number.isInteger(reactionCount) || reactionCount < 1) { + throw new RangeError("reaction count must be a positive integer"); + } + + let score = 0; + for (let index = 1; index <= reactionCount; index++) { + score += reactionWeight(index); + } + return score; +} + +export function totalScore(reactionCountsByUser: Iterable): number { + let total = 0; + for (const count of reactionCountsByUser) total += scoreForUser(count); + return total; +} diff --git a/src/scoring_test.ts b/src/scoring_test.ts new file mode 100644 index 0000000..b4c850f --- /dev/null +++ b/src/scoring_test.ts @@ -0,0 +1,19 @@ +import { assertAlmostEquals, assertThrows } from "@std/assert"; +import { reactionWeight, scoreForUser, totalScore } from "./scoring.ts"; + +Deno.test("one reaction by one user scores one point", () => { + assertAlmostEquals(scoreForUser(1), 1); +}); + +Deno.test("repeat reactions by one user have diminishing weight", () => { + assertAlmostEquals(reactionWeight(2), 1 / 3); + assertAlmostEquals(scoreForUser(3), 1 + 1 / 3 + 1 / 5); +}); + +Deno.test("total score sums each user's score", () => { + assertAlmostEquals(totalScore([1, 1, 2]), 3 + 1 / 3); +}); + +Deno.test("invalid reaction counts are rejected", () => { + assertThrows(() => scoreForUser(0), RangeError); +}); diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 94c5fba..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": [ - "ES2023" - ], - "module": "node16", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "allowImportingTsExtensions": true, - "noEmit": true, - - "outDir": "./dist", - }, - "include": [ - "src/**/*.ts" - ] -} \ No newline at end of file