diff --git a/package-lock.json b/package-lock.json index 15f8b330..a987279f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@discordjs/voice": "^0.19.0", "@js-temporal/polyfill": "^0.5.1", - "@napi-rs/canvas": "0.1.80", + "@napi-rs/canvas": "0.1.81", "@resvg/resvg-js": "^2.6.2", "@sentry/node": "^10.22.0", "@snazzah/davey": "^0.1.7", @@ -20,7 +20,7 @@ "chrono-node": "^2.9.0", "comment-json": "^4.4.1", "croner": "^9.1.0", - "discord.js": "^14.24.0", + "discord.js": "^14.24.1", "get-audio-duration": "^4.0.1", "graphviz-wasm": "^3.0.2", "jsdom": "^27.0.1", @@ -33,12 +33,12 @@ "youtube-dl-exec": "^3.0.26" }, "devDependencies": { - "@biomejs/biome": "^2.3.0", + "@biomejs/biome": "^2.3.2", "@types/better-sqlite3": "^7.6.13", "@types/jsdom": "^27.0.0", - "@types/node": "^24.9.1", + "@types/node": "^24.9.2", "@types/node-cron": "^3.0.11", - "@typescript/native-preview": "^7.0.0-dev.20251024.1", + "@typescript/native-preview": "^7.0.0-dev.20251027.1", "expect": "^30.2.0", "lefthook": "^2.0.1" }, @@ -121,9 +121,9 @@ } }, "node_modules/@biomejs/biome": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.0.tgz", - "integrity": "sha512-shdUY5H3S3tJVUWoVWo5ua+GdPW5lRHf+b0IwZ4OC1o2zOKQECZ6l2KbU6t89FNhtd3Qx5eg5N7/UsQWGQbAFw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.2.tgz", + "integrity": "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==", "dev": true, "license": "MIT OR Apache-2.0", "bin": { @@ -137,20 +137,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.3.0", - "@biomejs/cli-darwin-x64": "2.3.0", - "@biomejs/cli-linux-arm64": "2.3.0", - "@biomejs/cli-linux-arm64-musl": "2.3.0", - "@biomejs/cli-linux-x64": "2.3.0", - "@biomejs/cli-linux-x64-musl": "2.3.0", - "@biomejs/cli-win32-arm64": "2.3.0", - "@biomejs/cli-win32-x64": "2.3.0" + "@biomejs/cli-darwin-arm64": "2.3.2", + "@biomejs/cli-darwin-x64": "2.3.2", + "@biomejs/cli-linux-arm64": "2.3.2", + "@biomejs/cli-linux-arm64-musl": "2.3.2", + "@biomejs/cli-linux-x64": "2.3.2", + "@biomejs/cli-linux-x64-musl": "2.3.2", + "@biomejs/cli-win32-arm64": "2.3.2", + "@biomejs/cli-win32-x64": "2.3.2" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.0.tgz", - "integrity": "sha512-3cJVT0Z5pbTkoBmbjmDZTDFYxIkRcrs9sYVJbIBHU8E6qQxgXAaBfSVjjCreG56rfDuQBr43GzwzmaHPcu4vlw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz", + "integrity": "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==", "cpu": [ "arm64" ], @@ -165,9 +165,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.0.tgz", - "integrity": "sha512-6LIkhglh3UGjuDqJXsK42qCA0XkD1Ke4K/raFOii7QQPbM8Pia7Qj2Hji4XuF2/R78hRmEx7uKJH3t/Y9UahtQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz", + "integrity": "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==", "cpu": [ "x64" ], @@ -182,9 +182,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.0.tgz", - "integrity": "sha512-uhAsbXySX7xsXahegDg5h3CDgfMcRsJvWLFPG0pjkylgBb9lErbK2C0UINW52zhwg0cPISB09lxHPxCau4e2xA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz", + "integrity": "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==", "cpu": [ "arm64" ], @@ -199,9 +199,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.0.tgz", - "integrity": "sha512-nDksoFdwZ2YrE7NiYDhtMhL2UgFn8Kb7Y0bYvnTAakHnqEdb4lKindtBc1f+xg2Snz0JQhJUYO7r9CDBosRU5w==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz", + "integrity": "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==", "cpu": [ "arm64" ], @@ -216,9 +216,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.0.tgz", - "integrity": "sha512-uxa8reA2s1VgoH8MhbGlCmMOt3JuSE1vJBifkh1ulaPiuk0SPx8cCdpnm9NWnTe2x/LfWInWx4sZ7muaXTPGGw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz", + "integrity": "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==", "cpu": [ "x64" ], @@ -233,9 +233,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.0.tgz", - "integrity": "sha512-+i9UcJwl99uAhtRQDz9jUAh+Xkb097eekxs/D9j4deWDg5/yB/jPWzISe1nBHvlzTXsdUSj0VvB4Go2DSpKIMw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz", + "integrity": "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==", "cpu": [ "x64" ], @@ -250,9 +250,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.0.tgz", - "integrity": "sha512-ynjmsJLIKrAjC3CCnKMMhzcnNy8dbQWjKfSU5YA0mIruTxBNMbkAJp+Pr2iV7/hFou+66ZSD/WV8hmLEmhUaXA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz", + "integrity": "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==", "cpu": [ "arm64" ], @@ -267,9 +267,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.0.tgz", - "integrity": "sha512-zOCYmCRVkWXc9v8P7OLbLlGGMxQTKMvi+5IC4v7O8DkjLCOHRzRVK/Lno2pGZNo0lzKM60pcQOhH8HVkXMQdFg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz", + "integrity": "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==", "cpu": [ "x64" ], @@ -1240,9 +1240,9 @@ } }, "node_modules/@napi-rs/canvas": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", - "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.81.tgz", + "integrity": "sha512-ReCjd5SYI/UKx/olaQLC4GtN6wUQGjlgHXs1lvUvWGXfBMR3Fxnik3cL+OxKN5ithNdoU0/GlCrdKcQDFh2XKQ==", "license": "MIT", "workspaces": [ "e2e/*" @@ -1251,22 +1251,22 @@ "node": ">= 10" }, "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.80", - "@napi-rs/canvas-darwin-arm64": "0.1.80", - "@napi-rs/canvas-darwin-x64": "0.1.80", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", - "@napi-rs/canvas-linux-arm64-musl": "0.1.80", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-gnu": "0.1.80", - "@napi-rs/canvas-linux-x64-musl": "0.1.80", - "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + "@napi-rs/canvas-android-arm64": "0.1.81", + "@napi-rs/canvas-darwin-arm64": "0.1.81", + "@napi-rs/canvas-darwin-x64": "0.1.81", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.81", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.81", + "@napi-rs/canvas-linux-arm64-musl": "0.1.81", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.81", + "@napi-rs/canvas-linux-x64-gnu": "0.1.81", + "@napi-rs/canvas-linux-x64-musl": "0.1.81", + "@napi-rs/canvas-win32-x64-msvc": "0.1.81" } }, "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", - "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.81.tgz", + "integrity": "sha512-78Lz+AUi+MsWupyZjXwpwQrp1QCwncPvRZrdvrROcZ9Gq9grP7LfQZiGdR8LKyHIq3OR18mDP+JESGT15V1nXw==", "cpu": [ "arm64" ], @@ -1280,9 +1280,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", - "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.81.tgz", + "integrity": "sha512-omejuKgHWKDGoh8rsgsyhm/whwxMaryTQjJTd9zD7hiB9/rzcEEJLHnzXWR5ysy4/tTjHaQotE6k2t8eodTLnA==", "cpu": [ "arm64" ], @@ -1296,9 +1296,9 @@ } }, "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", - "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.81.tgz", + "integrity": "sha512-EYfk+co6BElq5DXNH9PBLYDYwc4QsvIVbyrsVHsxVpn4p6Y3/s8MChgC69AGqj3vzZBQ1qx2CRCMtg5cub+XuQ==", "cpu": [ "x64" ], @@ -1312,9 +1312,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", - "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.81.tgz", + "integrity": "sha512-teh6Q74CyAcH31yLNQGR9MtXSFxlZa5CI6vvNUISI14gWIJWrhOwUAOly+KRe1aztWR0FWTVSPxM4p5y+06aow==", "cpu": [ "arm" ], @@ -1328,9 +1328,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", - "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.81.tgz", + "integrity": "sha512-AGEopHFYRzJOjxY+2G1RmHPRnuWvO3Qdhq7sIazlSjxb3Z6dZHg7OB/4ZimXaimPjDACm9qWa6t5bn9bhXvkcw==", "cpu": [ "arm64" ], @@ -1344,9 +1344,9 @@ } }, "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", - "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.81.tgz", + "integrity": "sha512-Bj3m1cl4GIhsigkdwOxii4g4Ump3/QhNpx85IgAlCCYXpaly6mcsWpuDYEabfIGWOWhDUNBOndaQUPfWK1czOQ==", "cpu": [ "arm64" ], @@ -1360,9 +1360,9 @@ } }, "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", - "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.81.tgz", + "integrity": "sha512-yg/5NkHykVdwPlD3XObwCa/EswkOwLHswJcI9rHrac+znHsmCSj5AMX/RTU9Z9F6lZTwL60JM2Esit33XhAMiw==", "cpu": [ "riscv64" ], @@ -1376,9 +1376,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", - "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.81.tgz", + "integrity": "sha512-tPfMpSEBuV5dJSKexO/UZxpOqnYTaNbG8aKa1ek8QsWu+4SJ/foWkaxscra/RUv85vepx6WWDjzBNbNJsTnO0w==", "cpu": [ "x64" ], @@ -1392,9 +1392,9 @@ } }, "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", - "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.81.tgz", + "integrity": "sha512-1L0xnYgzqn8Baef+inPvY4dKqdmw3KCBoe0NEDgezuBZN7MA5xElwifoG8609uNdrMtJ9J6QZarsslLRVqri7g==", "cpu": [ "x64" ], @@ -1408,9 +1408,9 @@ } }, "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.80", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", - "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.81.tgz", + "integrity": "sha512-57ryVbhm/z7RE9/UVcS7mrLPdlayLesy+9U0Uf6epCoeSGrs99tfieCcgZWFbIgmByQ1AZnNtFI2N6huqDLlWQ==", "cpu": [ "x64" ], @@ -2708,9 +2708,9 @@ } }, "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2799,28 +2799,28 @@ "license": "MIT" }, "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-ZoJSN/ymGSBtNim3QQt85UmEHk4rN93fL6wiNHoN/84f/ZD/1RRbCQqo885DnQy+doh8fes90E7a2iJGKlU9ng==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-djbOSIm8Or967wMuO209ydMp2nq34hEulah1EhjUsLSqLplsbOk8RSOyVJJphU+CMP33rULDcnDAzvylU8Tq9Q==", "dev": true, "license": "Apache-2.0", "bin": { "tsgo": "bin/tsgo.js" }, "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251024.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251024.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20251024.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251024.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20251024.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251024.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20251024.1" + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251027.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251027.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20251027.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251027.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20251027.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251027.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20251027.1" } }, "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-r6fQMgUlNmpJBw0DOt+pxaLSoSeH7GraK4c9Q/HCaAaPO6AjwkgadURhj4AMpxzdTE62Ra3Xs28q0N2ybXTC+A==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-4Nysrmep6Z4C722nQF07XkEk22qyI2/vCfvfPSlhOxpJJcIFAroxSkSH7Qy8EDZWhNer9D4CMTYX9q5I8B75lQ==", "cpu": [ "arm64" ], @@ -2832,9 +2832,9 @@ ] }, "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-bI+oyej2pY/k0AZhYdSbvxuUACVenXRhEjqAbjxBivElqk6gfsTTItMPZRVgRP0QLlKsEPVI75/jaugewqKkJw==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-WvHLb6Mry214ZTuhfvv6fP1FLgYZ4oTw55+B2hTAo/O6qq9KX3OW90dvFYSMJKPhgvWR5B9tIEcMkIXGjxfv1w==", "cpu": [ "x64" ], @@ -2846,9 +2846,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-WFclQefBXD+bfD3vhZpPUc01dQnQ1GLZj8K5ksGEqHLTQF5xH5C9aa2yGia4BXWuOT1PIYX+/AyM7sa+lXuX+g==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-epAynE0qbU9nuPwaOgr9N6WANoYAdwhyteNB+PG2qRWYoFDYPXSgParjO1FAkY0uMt88QaS6vQ6ZglInHsxvXQ==", "cpu": [ "arm" ], @@ -2860,9 +2860,9 @@ ] }, "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-HjfUIyGGGcA8os/MpS2AX+JowqEIVsra7d95S5oAKGpcGBjZCggH/bn9DvijIp42D+eXYuGVA1OwL0hgLWLLSw==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-CNbTvppx8wsoRS3g4RcpDapRp4tNYp1eu+94HmtKT7ch3RJOliKIhAa/8odXIrkqnT+kc0wrQCzFiICMW4YieQ==", "cpu": [ "arm64" ], @@ -2874,9 +2874,9 @@ ] }, "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-I3/00X+yh5esr4F7cq4Qis5JKeznNBzIWjQDY/KnPyi3pRcEhXNlF38C0Cle6pxis3SQTRIZ548DfWmrq0KtFA==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-lzSUTdWYfKvsQJPQF/BtYil1Xmzn0f3jpgk8/4uVg4NQeDtzW0J3ceWl2lw1TuGnhISq2dwyupjKJfLQhe4AVQ==", "cpu": [ "x64" ], @@ -2888,9 +2888,9 @@ ] }, "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-8tWfDf/7dGXZXQaaK+l0G8qJoYhnRe2bN+CH87LCvuw5crzd/jJYiHW1VUxTSTYsS0mAanJ0PvXcDYu7BrjDZQ==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-K9K8t3HW/35ejgVJALPW9Fqo0PHOxh1/ir01C8r5qbhIdPQqwGlBHAGwLzrfH0ZF1R2nR2X4T+z+gB8tLULsow==", "cpu": [ "arm64" ], @@ -2902,9 +2902,9 @@ ] }, "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20251024.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251024.1.tgz", - "integrity": "sha512-cYIc6FiS5xCDNBzs9X0MuV/IS1sfg5WV//m9u7PcYLOv4zdQsHsEfqneTddtAjhjk2fiagO8PAkEEnT0CGd4jg==", + "version": "7.0.0-dev.20251027.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251027.1.tgz", + "integrity": "sha512-n7hb7ZjAEgoNBWYSt87+eMtSK2h6Xl9NWUd2ocw3Znz/tw8lwpUaG35FVd/Aj72kT1/5kiCBlM+7MxA214KGiw==", "cpu": [ "x64" ], @@ -3551,9 +3551,9 @@ ] }, "node_modules/discord.js": { - "version": "14.24.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.0.tgz", - "integrity": "sha512-KNq/ekT8bsmT3ZAfVre8cPbl+DfVYSdlLnDmGZPoz7Cw21LYeWHllRA9MivqNq5b1GPGAxGvyUN1vxbTb/PQWw==", + "version": "14.24.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.1.tgz", + "integrity": "sha512-LzL+MTGxB9mBwD8FjvkMwcIL4UtgG04e713U3+euqPCvOphhoVEoPpUNTvBPw4iJOas2uiuuh3JcveYSxIn8Tg==", "license": "Apache-2.0", "dependencies": { "@discordjs/builders": "^1.13.0", diff --git a/package.json b/package.json index 41efed02..6a2c299a 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "dependencies": { "@discordjs/voice": "^0.19.0", "@js-temporal/polyfill": "^0.5.1", - "@napi-rs/canvas": "0.1.80", + "@napi-rs/canvas": "0.1.81", "@resvg/resvg-js": "^2.6.2", "@sentry/node": "^10.22.0", "@snazzah/davey": "^0.1.7", @@ -36,7 +36,7 @@ "chrono-node": "^2.9.0", "comment-json": "^4.4.1", "croner": "^9.1.0", - "discord.js": "^14.24.0", + "discord.js": "^14.24.1", "get-audio-duration": "^4.0.1", "graphviz-wasm": "^3.0.2", "jsdom": "^27.0.1", @@ -49,12 +49,12 @@ "youtube-dl-exec": "^3.0.26" }, "devDependencies": { - "@biomejs/biome": "^2.3.0", + "@biomejs/biome": "^2.3.2", "@types/better-sqlite3": "^7.6.13", "@types/jsdom": "^27.0.0", - "@types/node": "^24.9.1", + "@types/node": "^24.9.2", "@types/node-cron": "^3.0.11", - "@typescript/native-preview": "^7.0.0-dev.20251024.1", + "@typescript/native-preview": "^7.0.0-dev.20251027.1", "expect": "^30.2.0", "lefthook": "^2.0.1" }, diff --git a/src/commands/poll.ts b/src/commands/poll.ts index 69e861d0..5e5638f9 100644 --- a/src/commands/poll.ts +++ b/src/commands/poll.ts @@ -81,17 +81,12 @@ Optionen: return "Bruder da ist keine Umfrage :c"; } - const pollArray = pollService.parsePollOptionString(positionals.join(" ")); - - const question = pollArray[0]; + const [question, ...pollOptions] = pollService.parsePollOptionString(positionals.join(" ")); if (question.length > pollEmbedService.TEXT_LIMIT) { return "Bruder die Frage ist ja länger als mein Schwands :c"; } - const pollOptions = pollArray.slice(1); let pollOptionsTextLength = 0; - - const isExtendable = options.extendable; for (const pollOption of pollOptions) { pollOptionsTextLength += pollOption.length; } @@ -100,7 +95,7 @@ Optionen: return "Bruder da sind keine Antwortmöglichkeiten :c"; } - if (pollOptions.length < 2 && !isExtendable) { + if (pollOptions.length < 2 && !options.extendable) { return "Bruder du musst schon mehr als eine Antwortmöglichkeit geben 🙄"; } diff --git a/src/handler/reaction/pollReactionHandler.ts b/src/handler/reaction/pollReactionHandler.ts index 95194724..16cccaa6 100644 --- a/src/handler/reaction/pollReactionHandler.ts +++ b/src/handler/reaction/pollReactionHandler.ts @@ -42,8 +42,12 @@ export default { return; } - const validVoteReactions = dbPoll.multipleChoices ? POLL_EMOJIS : VOTE_EMOJIS; - if (!validVoteReactions.includes(reactionName)) { + if (VOTE_EMOJIS.includes(reactionName)) { + // this is a .vote poll -> TODO + return; + } + + if (!POLL_EMOJIS.includes(reactionName)) { return; } diff --git a/src/service/poll.ts b/src/service/poll.ts index 6c874833..8fab166f 100644 --- a/src/service/poll.ts +++ b/src/service/poll.ts @@ -7,9 +7,10 @@ import type { BotContext } from "@/context.js"; import * as polls from "@/storage/poll.js"; import * as fadingMessage from "@/storage/fadingMessage.js"; import * as additionalMessageData from "@/storage/additionalMessageData.js"; -import type { ProcessableMessage } from "./command.js"; import { EMOJI } from "@/service/pollEmbed.js"; +import log from "@log"; + export const POLL_EMOJIS = EMOJI; export const VOTE_EMOJIS = ["👍", "👎"]; @@ -94,47 +95,25 @@ export async function countDelayedVote( return; } - const reactionName = reaction.emoji.name; - if (reactionName === null) { - throw new Error("Could not find reaction name"); - } - - if (poll.multipleChoices) { - // TODO: Toogle user vote with DB backing - - // Old code: - const delayedPollReactions = delayedPoll.reactions[VOTE_EMOJIS.indexOf(reactionName)]; - const hasVoted = delayedPollReactions.some(x => x === invoker.id); - if (!hasVoted) { - delayedPollReactions.push(invoker.id); - } else { - delayedPollReactions.splice(delayedPollReactions.indexOf(invoker.id), 1); - } - - const msg = await message.channel.send( - hasVoted ? "🗑 Deine Reaktion wurde gelöscht." : "💾 Deine Reaktion wurde gespeichert.", - ); - await fadingMessage.startFadingMessage(msg as ProcessableMessage, 2500); - } else { - // TODO: Set user vote with DB backing - - // Old code: - for (const reactionList of delayedPoll.reactions) { - reactionList.forEach((x, i) => { - if (x === invoker.id) reactionList.splice(i); - }); - } - const delayedPollReactions = delayedPoll.reactions[POLL_EMOJIS.indexOf(reactionName)]; - delayedPollReactions.push(invoker.id); + const optionIndex = determineOptionIndex(reaction); + if (optionIndex === undefined) { + return; } - // It's a delayed poll, we clear all Reactions - const allUserReactions = message.reactions.cache.filter(r => { - const emojiName = r.emoji.name; - return emojiName && r.users.cache.has(invoker.id) && POLL_EMOJIS.includes(emojiName); - }); + const addedOrRemoved = await polls.addOrToggleAnswer( + poll.id, + optionIndex, + invoker.id, + !poll.multipleChoices, + ); - await Promise.allSettled(allUserReactions.map(r => r.users.remove(invoker.id))); + const msg = await message.channel.send( + addedOrRemoved === "removed" + ? "🗑 Deine Reaktion wurde gelöscht." + : "💾 Deine Reaktion wurde gespeichert.", + ); + await fadingMessage.startFadingMessage(msg, 2500); + await removeAllReactions(message, invoker); await additionalMessageData.upsertForMessage( message, @@ -143,29 +122,75 @@ export async function countDelayedVote( ); } +async function removeAllReactions(message: Message, invoker: GuildMember) { + const allUserReactions = message.reactions.cache.filter(r => { + const emojiName = r.emoji.name; + return emojiName && POLL_EMOJIS.includes(emojiName); + }); + + await Promise.allSettled(allUserReactions.map(r => r.users.remove(invoker.id))); +} + export async function countVote( poll: Poll, - message: Message, + _message: Message, invoker: GuildMember, reaction: MessageReaction, ) { console.assert(poll.endsAt === null, "Poll is a delayed poll"); - // TODO: Set user vote with DB backing - // Old code: - return await Promise.allSettled( + const optionIndex = determineOptionIndex(reaction); + if (optionIndex === undefined) { + log.info(reaction, "Unknown option index"); // TODO: Remove + return; + } + + await polls.addOrToggleAnswer(poll.id, optionIndex, invoker.id, !poll.multipleChoices); + log.info("Counted vote"); + + if (poll.multipleChoices) { + return; + } + + await removeAllOtherReactionsFromUser(invoker, reaction); +} + +async function removeAllOtherReactionsFromUser( + invoker: GuildMember, + reactionToKeep: MessageReaction, +): Promise { + const message = await reactionToKeep.message.fetch(); + + const nameToKeep = reactionToKeep.emoji.name; + if (nameToKeep === null || nameToKeep.length === 0) { + throw new Error("`nameToKeep` was null or empty."); + } + + const results = await Promise.allSettled( message.reactions.cache - .filter(r => { - const emojiName = r.emoji.name; - return ( - !!emojiName && - r.users.cache.has(invoker.id) && - emojiName !== reaction.emoji.name && - POLL_EMOJIS.includes(emojiName) - ); - }) - .map(reaction => reaction.users.remove(invoker.id)), + .filter( + r => + r.emoji.name && + r.emoji.name !== nameToKeep && + POLL_EMOJIS.includes(r.emoji.name), + ) + .map(r => r.users.remove(invoker.id)), ); + + const failedTasks = results.filter(r => r.status === "rejected").length; + if (failedTasks) { + throw new Error(`Failed to update ${failedTasks} reaction users`); + } +} + +function determineOptionIndex(reaction: MessageReaction) { + const reactionName = reaction.emoji.name; + if (reactionName === null) { + throw new Error("Reaction does not have a name."); + } + + const index = POLL_EMOJIS.indexOf(reactionName); + return index < 0 ? undefined : index; } export function parsePollOptionString(value: string): string[] { diff --git a/src/storage/db/model.ts b/src/storage/db/model.ts index 2fc4f922..ee13af82 100644 --- a/src/storage/db/model.ts +++ b/src/storage/db/model.ts @@ -38,6 +38,7 @@ export interface Database { pets: PetsTable; polls: PollsTable; pollOptions: PollOptionsTable; + pollAnswers: PollAnswersTable; } export type OneBasedMonth = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12; @@ -436,3 +437,14 @@ export interface PollOptionsTable extends AuditedTable { authorId: Snowflake; } + +export type PollAnswerId = number; + +export type PollAnswer = Selectable; + +export interface PollAnswersTable extends AuditedTable { + id: GeneratedAlways; + + optionId: PollOptionId; + userId: Snowflake; +} diff --git a/src/storage/fadingMessage.ts b/src/storage/fadingMessage.ts index cfe9c01a..7b33c4ec 100644 --- a/src/storage/fadingMessage.ts +++ b/src/storage/fadingMessage.ts @@ -1,9 +1,10 @@ -import type { ProcessableMessage } from "@/service/command.js"; +import type { Message } from "discord.js"; + import type { FadingMessage } from "./db/model.js"; import db from "@db"; export function startFadingMessage( - message: ProcessableMessage, + message: Message, deleteInMs: number, ctx = db(), ): Promise { @@ -13,7 +14,7 @@ export function startFadingMessage( .values({ beginTime: now.toISOString(), // adding milliseconds to a date is a hassle in sqlite, so we're doing it in JS - endTime: new Date(now.getTime() + deleteInMs).toDateString(), + endTime: new Date(now.getTime() + deleteInMs).toISOString(), guildId: message.guild.id, channelId: message.channel.id, messageId: message.id, diff --git a/src/storage/migrations/23-polls-answers.ts b/src/storage/migrations/23-polls-answers.ts new file mode 100644 index 00000000..daaf1ac5 --- /dev/null +++ b/src/storage/migrations/23-polls-answers.ts @@ -0,0 +1,41 @@ +import { sql, type Kysely } from "kysely"; + +export async function up(db: Kysely) { + await db.schema + .createTable("pollAnswers") + .addColumn("id", "integer", c => c.primaryKey().autoIncrement()) + .addColumn("optionId", "integer", c => + c.references("pollOptions.id").notNull().onDelete("cascade"), + ) + .addColumn("userId", "text", c => c.notNull()) + .addColumn("createdAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .addColumn("updatedAt", "timestamp", c => c.notNull().defaultTo(sql`current_timestamp`)) + .execute(); + + await db.schema + .createIndex("pollAnswers_optionId_userId_index") + .on("pollAnswers") + .columns(["optionId", "userId"]) + .unique() + .execute(); + + await createUpdatedAtTrigger(db, "pollAnswers"); +} + +function createUpdatedAtTrigger(db: Kysely, tableName: string) { + return sql + .raw(` + create trigger ${tableName}_updatedAt + after update on ${tableName} for each row + begin + update ${tableName} + set updatedAt = current_timestamp + where id = old.id; + end; + `) + .execute(db); +} + +export async function down(_db: Kysely) { + throw new Error("Not supported lol"); +} diff --git a/src/storage/poll.ts b/src/storage/poll.ts index e1c6bd6a..5d0ef566 100644 --- a/src/storage/poll.ts +++ b/src/storage/poll.ts @@ -2,7 +2,7 @@ import type { Snowflake } from "discord.js"; import type { Temporal } from "@js-temporal/polyfill"; import db from "@db"; -import type { Poll, PollId, PollOption } from "./db/model.js"; +import type { Poll, PollAnswer, PollId, PollOption, PollOptionId } from "./db/model.js"; export interface MessageLocation { guildId: Snowflake; @@ -194,3 +194,68 @@ export async function markPollAsEnded(pollId: PollId, ctx = db()): Promise .returningAll() .executeTakeFirstOrThrow(); } + +export async function addOrToggleAnswer( + pollId: PollId, + optionIndex: number, + userId: Snowflake, + removeOthers: boolean, + ctx = db(), +): Promise<"added" | "removed"> { + return await ctx.transaction().execute(async ctx => { + const { optionId } = await ctx + .selectFrom("pollOptions") + .where("pollId", "=", pollId) + .where("index", "=", optionIndex) + .select("id as optionId") + .executeTakeFirstOrThrow(); + + if (removeOthers) { + await ctx + .deleteFrom("pollAnswers") + .where("userId", "=", userId) + .where( + "optionId", + "in", + ctx + .selectFrom("pollOptions") + .select("id") + .where("pollId", "=", pollId) + .where("id", "!=", optionId), + ) + .execute(); + } + + const preExistingAnswer = await ctx + .deleteFrom("pollAnswers") + .where("optionId", "=", optionId) + .where("userId", "=", userId) + .returningAll() + .executeTakeFirst(); + + if (preExistingAnswer) { + // answer already existed and has been deleted + return "removed"; + } + + await ctx + .insertInto("pollAnswers") + .values({ optionId, userId }) + .returningAll() + .executeTakeFirstOrThrow(); + return "added"; + }); +} + +export async function removeAwnser( + optionId: PollOptionId, + userId: Snowflake, + ctx = db(), +): Promise { + return await ctx + .deleteFrom("pollAnswers") + .where("optionId", "=", optionId) + .where("userId", "=", userId) + .returningAll() + .executeTakeFirstOrThrow(); +}