From b60db69157c6e2564d32de9ae63d4a2856d8f195 Mon Sep 17 00:00:00 2001 From: Milo Date: Thu, 6 Nov 2025 02:48:36 +0100 Subject: [PATCH] fix: big fix + prettier --- .idea/codeStyles/Project.xml | 61 + .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/inspectionProfiles/Project_Default.xml | 2 +- .idea/jsLinters/eslint.xml | 6 + .idea/prettier.xml | 7 + .prettierrc | 4 + README.md | 2 + commands.js | 6 +- eslint.config.js | 9 + index.js | 56 +- package-lock.json | 6696 ++++++++++-------- package.json | 74 +- renovate.json | 12 +- src/api/discord.js | 80 +- src/api/valorant.js | 18 +- src/bot/client.js | 36 +- src/bot/commands/floposite.js | 82 +- src/bot/commands/info.js | 109 +- src/bot/commands/inventory.js | 259 +- src/bot/commands/search.js | 209 +- src/bot/commands/skins.js | 107 +- src/bot/commands/timeout.js | 396 +- src/bot/commands/valorant.js | 354 +- src/bot/components/inventoryNav.js | 291 +- src/bot/components/pollVote.js | 314 +- src/bot/components/searchNav.js | 205 +- src/bot/components/upgradeSkin.js | 382 +- src/bot/events.js | 72 +- src/bot/handlers/interactionCreate.js | 136 +- src/bot/handlers/messageCreate.js | 1161 ++- src/config/commands.js | 184 +- src/database/index.js | 481 +- src/game/blackjack.js | 623 +- src/game/elo.js | 252 +- src/game/points.js | 304 +- src/game/poker.js | 231 +- src/game/solitaire.js | 615 +- src/game/state.js | 4 +- src/game/various.js | 169 +- src/server/app.js | 59 +- src/server/routes/api.js | 1775 ++--- src/server/routes/blackjack.js | 614 +- src/server/routes/erinyes.js | 142 +- src/server/routes/market.js | 73 + src/server/routes/poker.js | 868 +-- src/server/routes/solitaire.js | 443 +- src/server/socket.js | 589 +- src/utils/ai.js | 270 +- src/utils/erinyes.js | 148 +- src/utils/index.js | 474 +- 50 files changed, 11102 insertions(+), 8367 deletions(-) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/jsLinters/eslint.xml create mode 100644 .idea/prettier.xml create mode 100644 .prettierrc create mode 100644 eslint.config.js create mode 100644 src/server/routes/market.js diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..f739e7f --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index fe16e3f..03d9549 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,6 @@ \ No newline at end of file diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml new file mode 100644 index 0000000..541945b --- /dev/null +++ b/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/prettier.xml b/.idea/prettier.xml new file mode 100644 index 0000000..0c83ac4 --- /dev/null +++ b/.idea/prettier.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3611ff3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "printWidth": 120, + "useTabs": true +} diff --git a/README.md b/README.md index 258441f..dc16ffb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # FLOPOBOT DEUXIEME DU NOM ## Project structure + Below is a basic overview of the project structure: ``` @@ -15,5 +16,6 @@ Below is a basic overview of the project structure: ``` ## FlopoSite + - FlopoBot has its own website to use it a different way [FlopoSite's repo](https://github.com/cassoule/floposite) diff --git a/commands.js b/commands.js index 869e87d..589067f 100644 --- a/commands.js +++ b/commands.js @@ -1,5 +1,5 @@ -import { registerCommands } from './src/config/commands.js'; +import { registerCommands } from "./src/config/commands.js"; -console.log('Registering global commands...'); +console.log("Registering global commands..."); registerCommands(); -console.log('Commands registered.'); \ No newline at end of file +console.log("Commands registered."); diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..4dec15d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,9 @@ +import globals from "globals"; +import json from "@eslint/json"; +import { defineConfig } from "eslint/config"; + +export default defineConfig([ + { files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.node } }, + { files: ["**/*.json"], plugins: { json }, language: "json/json" }, + { files: ["**/*.jsonc"], plugins: { json }, language: "json/jsonc" }, +]); diff --git a/index.js b/index.js index 70fc89b..ea7a0dd 100644 --- a/index.js +++ b/index.js @@ -1,53 +1,51 @@ -import 'dotenv/config'; -import http from 'http'; -import { Server } from 'socket.io'; +import "dotenv/config"; +import http from "http"; +import { Server } from "socket.io"; -import { app } from './src/server/app.js'; -import { client } from './src/bot/client.js'; -import { initializeEvents } from './src/bot/events.js'; -import { initializeSocket } from './src/server/socket.js'; -import { getAkhys, setupCronJobs } from './src/utils/index.js'; +import { app } from "./src/server/app.js"; +import { client } from "./src/bot/client.js"; +import { initializeEvents } from "./src/bot/events.js"; +import { initializeSocket } from "./src/server/socket.js"; +import { setupCronJobs } from "./src/utils/index.js"; // --- SERVER INITIALIZATION --- const PORT = process.env.PORT || 25578; const server = http.createServer(app); // --- SOCKET.IO INITIALIZATION --- -const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; +const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; export const io = new Server(server, { - cors: { - origin: FLAPI_URL, - methods: ['GET', 'POST', 'PUT', 'OPTIONS'], - }, - pingInterval: 5000, - pingTimeout: 5000, + cors: { + origin: FLAPI_URL, + methods: ["GET", "POST", "PUT", "OPTIONS"], + }, + pingInterval: 5000, + pingTimeout: 5000, }); initializeSocket(io, client); - // --- BOT INITIALIZATION --- initializeEvents(client, io); client.login(process.env.BOT_TOKEN).then(() => { - console.log(`Logged in as ${client.user.tag}`); - console.log('[Discord Bot Events Initialized]'); + console.log(`Logged in as ${client.user.tag}`); + console.log("[Discord Bot Events Initialized]"); }); - // --- APP STARTUP --- server.listen(PORT, async () => { - console.log(`Express+Socket.IO server listening on port ${PORT}`); - console.log(`[Connected with ${FLAPI_URL}]`); + console.log(`Express+Socket.IO server listening on port ${PORT}`); + console.log(`[Connected with ${FLAPI_URL}]`); - // Initial data fetch and setup - try { + // Initial data fetch and setup + /*try { await getAkhys(client); } catch (error) { console.log('Initial Fetch Error'); - } + }*/ - // Setup scheduled tasks - //setupCronJobs(client, io); - console.log('[Cron Jobs Initialized]'); + // Setup scheduled tasks + setupCronJobs(client, io); + console.log("[Cron Jobs Initialized]"); - console.log('--- FlopoBOT is ready ---'); -}); \ No newline at end of file + console.log("--- FlopoBOT is ready ---"); +}); diff --git a/package-lock.json b/package-lock.json index cbdf942..b98b9af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2800 +1,3900 @@ { - "name": "t12_flopobot", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "t12_flopobot", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@google/genai": "^0.8.0", - "@mistralai/mistralai": "^1.6.0", - "axios": "^1.9.0", - "better-sqlite3": "^11.9.1", - "discord-interactions": "^4.0.0", - "discord.js": "^14.18.0", - "dotenv": "^16.0.3", - "express": "^4.18.2", - "node-cron": "^3.0.3", - "openai": "^4.104.0", - "pokersolver": "^2.1.4", - "socket.io": "^4.8.1", - "unique-names-generator": "^4.7.1", - "uuid": "^11.1.0" - }, - "devDependencies": { - "nodemon": "^3.0.0" - }, - "engines": { - "node": ">=18.x" - } - }, - "node_modules/@discordjs/builders": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.3.tgz", - "integrity": "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/formatters": "^0.6.1", - "@discordjs/util": "^1.1.1", - "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.16", - "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.4", - "tslib": "^2.6.3" - }, - "engines": { - "node": ">=16.11.0" - }, - "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==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.11.0" - } - }, - "node_modules/@discordjs/formatters": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", - "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", - "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.1" - }, - "engines": { - "node": ">=16.11.0" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/rest": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", - "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", - "license": "Apache-2.0", - "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.38.16", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.21.3" - }, - "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==", - "license": "Apache-2.0", - "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==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@discordjs/ws": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", - "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/collection": "^2.1.0", - "@discordjs/rest": "^2.5.1", - "@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.38.1", - "tslib": "^2.6.2", - "ws": "^8.17.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==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/discordjs/discord.js?sponsor" - } - }, - "node_modules/@google/genai": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.8.0.tgz", - "integrity": "sha512-Zs+OGyZKyMbFofGJTR9/jTQSv8kITh735N3tEuIZj4VlMQXTC0soCFahysJ9NaeenRlD7xGb6fyqmX+FwrpU6Q==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^9.14.2", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@mistralai/mistralai": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.6.0.tgz", - "integrity": "sha512-PQwGV3+n7FbE7Dp3Vnd8DAa3ffx6WuVV966Gfmf4QvzwcO3Mvxpz0SnJ/PjaZcsCwApBCZpNyQzvarAKEQLKeQ==", - "dependencies": { - "zod-to-json-schema": "^3.24.1" - }, - "peerDependencies": { - "zod": ">= 3" - } - }, - "node_modules/@sapphire/async-queue": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "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==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", - "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "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==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", - "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "license": "MIT", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/better-sqlite3": { - "version": "11.9.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", - "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - } - }, - "node_modules/bignumber.js": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.1.tgz", - "integrity": "sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/discord-api-types": { - "version": "0.38.23", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.23.tgz", - "integrity": "sha512-C8VjK0yxBUq1dakxGpUXQm4VSC7R+aaD2SIr3paj2a0bP/LRok1AqHiezp30GruK6Ba9FtQAKqYUMJPzsqv7IQ==", - "license": "MIT", - "workspaces": [ - "scripts/actions/documentation" - ] - }, - "node_modules/discord-interactions": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/discord-interactions/-/discord-interactions-4.1.1.tgz", - "integrity": "sha512-dCrdJFSHQrEHXB8tk+LpIhAL7/+/UXCFDzooYmB6jj7b/zuhdbETpcZXLYQpSfcSRpF5wjmnu1XwuHlvdF1KpQ==", - "license": "MIT", - "engines": { - "node": ">=18.4.0" - } - }, - "node_modules/discord.js": { - "version": "14.22.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.22.1.tgz", - "integrity": "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==", - "license": "Apache-2.0", - "dependencies": { - "@discordjs/builders": "^1.11.2", - "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.1", - "@discordjs/rest": "^2.6.0", - "@discordjs/util": "^1.1.1", - "@discordjs/ws": "^1.2.3", - "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.16", - "fast-deep-equal": "3.1.3", - "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.10.0", - "tslib": "^2.6.3", - "undici": "6.21.3" - }, - "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==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", - "license": "MIT", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "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 - } - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "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, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gaxios/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/magic-bytes.js": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", - "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.74.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", - "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-cron": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", - "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", - "license": "ISC", - "dependencies": { - "uuid": "8.3.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/node-cron/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/nodemon": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", - "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.86", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", - "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/openai/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pokersolver": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/pokersolver/-/pokersolver-2.1.4.tgz", - "integrity": "sha512-vmgZS+K8H8r1RePQykFM5YyvlKo1v3xVec8FMBjg9N6mR2Tj/n/X415w+lG67FWbrk71D/CADmKFinDgaQlAsw==", - "engines": [ - "node >= 4.0.0" - ], - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "license": "MIT", - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-adapter/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "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 - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "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==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "6.21.3", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", - "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unique-names-generator": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", - "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - } - } + "name": "t12_flopobot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "t12_flopobot", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@google/genai": "^0.8.0", + "@mistralai/mistralai": "^1.6.0", + "axios": "^1.9.0", + "better-sqlite3": "^11.9.1", + "discord-interactions": "^4.0.0", + "discord.js": "^14.18.0", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "node-cron": "^3.0.3", + "openai": "^4.104.0", + "pokersolver": "^2.1.4", + "socket.io": "^4.8.1", + "unique-names-generator": "^4.7.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@eslint/json": "^0.14.0", + "eslint": "^9.39.1", + "globals": "^16.5.0", + "nodemon": "^3.0.0", + "prettier": "3.6.2" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.3.tgz", + "integrity": "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.16", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "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.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "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==", + "license": "Apache-2.0", + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@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.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.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==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/json": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.14.0.tgz", + "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "@eslint/plugin-kit": "^0.4.1", + "@humanwhocodes/momoa": "^3.3.10", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@google/genai": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.8.0.tgz", + "integrity": "sha512-Zs+OGyZKyMbFofGJTR9/jTQSv8kITh735N3tEuIZj4VlMQXTC0soCFahysJ9NaeenRlD7xGb6fyqmX+FwrpU6Q==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz", + "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.6.0.tgz", + "integrity": "sha512-PQwGV3+n7FbE7Dp3Vnd8DAa3ffx6WuVV966Gfmf4QvzwcO3Mvxpz0SnJ/PjaZcsCwApBCZpNyQzvarAKEQLKeQ==", + "dependencies": { + "zod-to-json-schema": "^3.24.1" + }, + "peerDependencies": { + "zod": ">= 3" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/better-sqlite3": { + "version": "11.9.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.9.1.tgz", + "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bignumber.js": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.1.tgz", + "integrity": "sha512-+NzaKgOUvInq9TIUZ1+DRspzf/HApkCwD4btfuasFTdrfnOxqx853TgDpMolp+uv4RpRp7bPcEU2zKr9+fRmyw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.38.23", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.23.tgz", + "integrity": "sha512-C8VjK0yxBUq1dakxGpUXQm4VSC7R+aaD2SIr3paj2a0bP/LRok1AqHiezp30GruK6Ba9FtQAKqYUMJPzsqv7IQ==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord-interactions": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/discord-interactions/-/discord-interactions-4.1.1.tgz", + "integrity": "sha512-dCrdJFSHQrEHXB8tk+LpIhAL7/+/UXCFDzooYmB6jj7b/zuhdbETpcZXLYQpSfcSRpF5wjmnu1XwuHlvdF1KpQ==", + "license": "MIT", + "engines": { + "node": ">=18.4.0" + } + }, + "node_modules/discord.js": { + "version": "14.22.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.22.1.tgz", + "integrity": "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.11.2", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.16", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "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==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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 + } + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodemon": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", + "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.86", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", + "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pokersolver": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/pokersolver/-/pokersolver-2.1.4.tgz", + "integrity": "sha512-vmgZS+K8H8r1RePQykFM5YyvlKo1v3xVec8FMBjg9N6mR2Tj/n/X415w+lG67FWbrk71D/CADmKFinDgaQlAsw==", + "engines": [ + "node >= 4.0.0" + ], + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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 + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unique-names-generator": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz", + "integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } } diff --git a/package.json b/package.json index 55469f2..7241477 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,41 @@ { - "name": "t12_flopobot", - "private": true, - "version": "1.0.0", - "description": "Flopobot le 2 donc en mieux", - "main": "index.js", - "type": "module", - "engines": { - "node": ">=18.x" - }, - "scripts": { - "start": "node index.js", - "register": "node commands.js", - "dev": "nodemon index.js" - }, - "author": "Milo Gourvest", - "license": "MIT", - "dependencies": { - "@google/genai": "^0.8.0", - "@mistralai/mistralai": "^1.6.0", - "axios": "^1.9.0", - "better-sqlite3": "^11.9.1", - "discord-interactions": "^4.0.0", - "discord.js": "^14.18.0", - "dotenv": "^16.0.3", - "express": "^4.18.2", - "node-cron": "^3.0.3", - "openai": "^4.104.0", - "pokersolver": "^2.1.4", - "socket.io": "^4.8.1", - "unique-names-generator": "^4.7.1", - "uuid": "^11.1.0" - }, - "devDependencies": { - "nodemon": "^3.0.0" - } + "name": "t12_flopobot", + "private": true, + "version": "1.0.0", + "description": "Flopobot le 2 donc en mieux", + "main": "index.js", + "type": "module", + "engines": { + "node": ">=18.x" + }, + "scripts": { + "start": "node index.js", + "register": "node commands.js", + "dev": "nodemon index.js" + }, + "author": "Milo Gourvest", + "license": "MIT", + "dependencies": { + "@google/genai": "^0.8.0", + "@mistralai/mistralai": "^1.6.0", + "axios": "^1.9.0", + "better-sqlite3": "^11.9.1", + "discord-interactions": "^4.0.0", + "discord.js": "^14.18.0", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "node-cron": "^3.0.3", + "openai": "^4.104.0", + "pokersolver": "^2.1.4", + "socket.io": "^4.8.1", + "unique-names-generator": "^4.7.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@eslint/json": "^0.14.0", + "eslint": "^9.39.1", + "globals": "^16.5.0", + "nodemon": "^3.0.0", + "prettier": "3.6.2" + } } diff --git a/renovate.json b/renovate.json index 821c3fc..781bc2f 100644 --- a/renovate.json +++ b/renovate.json @@ -1,11 +1,5 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ - "config:recommended", - ":disableDependencyDashboard", - ":preserveSemverRanges" - ], - "ignorePaths": [ - "**/node_modules/**" - ] + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended", ":disableDependencyDashboard", ":preserveSemverRanges"], + "ignorePaths": ["**/node_modules/**"] } diff --git a/src/api/discord.js b/src/api/discord.js index 9699eee..4bbddcb 100644 --- a/src/api/discord.js +++ b/src/api/discord.js @@ -1,4 +1,4 @@ -import 'dotenv/config'; +import "dotenv/config"; /** * A generic function for making requests to the Discord API. @@ -10,38 +10,38 @@ import 'dotenv/config'; * @throws Will throw an error if the API request is not successful. */ export async function DiscordRequest(endpoint, options) { - // Construct the full API URL - const url = 'https://discord.com/api/v10/' + endpoint; + // Construct the full API URL + const url = "https://discord.com/api/v10/" + endpoint; - // Stringify the payload if it exists - if (options && options.body) { - options.body = JSON.stringify(options.body); - } + // Stringify the payload if it exists + if (options && options.body) { + options.body = JSON.stringify(options.body); + } - // Use fetch to make the request, automatically including required headers - const res = await fetch(url, { - headers: { - 'Authorization': `Bot ${process.env.DISCORD_TOKEN}`, - 'Content-Type': 'application/json; charset=UTF-8', - 'User-Agent': 'DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)', - }, - ...options, // Spread the given options (e.g., method, body) - }); + // Use fetch to make the request, automatically including required headers + const res = await fetch(url, { + headers: { + Authorization: `Bot ${process.env.DISCORD_TOKEN}`, + "Content-Type": "application/json; charset=UTF-8", + "User-Agent": "DiscordBot (https://github.com/discord/discord-example-app, 1.0.0)", + }, + ...options, // Spread the given options (e.g., method, body) + }); - // If the request was not successful, throw a detailed error - if (!res.ok) { - let data - try { - data = await res.json(); - } catch (err) { - data = res; - } - console.error(`Discord API Error on endpoint ${endpoint}:`, res.status, data); - throw new Error(JSON.stringify(data)); - } + // If the request was not successful, throw a detailed error + if (!res.ok) { + let data; + try { + data = await res.json(); + } catch (err) { + data = res; + } + console.error(`Discord API Error on endpoint ${endpoint}:`, res.status, data); + throw new Error(JSON.stringify(data)); + } - // Return the original response object for further processing - return res; + // Return the original response object for further processing + return res; } /** @@ -51,15 +51,15 @@ export async function DiscordRequest(endpoint, options) { * @param {Array} commands - An array of command objects to install. */ export async function InstallGlobalCommands(appId, commands) { - // API endpoint for bulk overwriting global commands - const endpoint = `applications/${appId}/commands`; + // API endpoint for bulk overwriting global commands + const endpoint = `applications/${appId}/commands`; - console.log('Installing global commands...'); - try { - // This uses the generic DiscordRequest function to make the API call - await DiscordRequest(endpoint, { method: 'PUT', body: commands }); - console.log('Successfully installed global commands.'); - } catch (err) { - console.error('Error installing global commands:', err); - } -} \ No newline at end of file + console.log("Installing global commands..."); + try { + // This uses the generic DiscordRequest function to make the API call + await DiscordRequest(endpoint, { method: "PUT", body: commands }); + console.log("Successfully installed global commands."); + } catch (err) { + console.error("Error installing global commands:", err); + } +} diff --git a/src/api/valorant.js b/src/api/valorant.js index f88b875..ccf8cf2 100644 --- a/src/api/valorant.js +++ b/src/api/valorant.js @@ -1,11 +1,11 @@ -export async function getValorantSkins(locale='fr-FR') { - const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: 'GET' }); - const data = await response.json(); - return data.data +export async function getValorantSkins(locale = "fr-FR") { + const response = await fetch(`https://valorant-api.com/v1/weapons/skins?language=${locale}`, { method: "GET" }); + const data = await response.json(); + return data.data; } -export async function getSkinTiers(locale='fr-FR') { - const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: 'GET'}); - const data = await response.json(); - return data.data -} \ No newline at end of file +export async function getSkinTiers(locale = "fr-FR") { + const response = await fetch(`https://valorant-api.com/v1/contenttiers?language=${locale}`, { method: "GET" }); + const data = await response.json(); + return data.data; +} diff --git a/src/bot/client.js b/src/bot/client.js index f4c7939..3671ea4 100644 --- a/src/bot/client.js +++ b/src/bot/client.js @@ -1,28 +1,28 @@ -import { Client, GatewayIntentBits } from 'discord.js'; +import { Client, GatewayIntentBits } from "discord.js"; /** * The single, shared Discord.js Client instance for the entire application. * It is configured with all the necessary intents to receive the events it needs. */ export const client = new Client({ - // Define the events the bot needs to receive from Discord's gateway. - intents: [ - // Required for basic guild information and events. - GatewayIntentBits.Guilds, + // Define the events the bot needs to receive from Discord's gateway. + intents: [ + // Required for basic guild information and events. + GatewayIntentBits.Guilds, - // Required to receive messages in guilds (e.g., in #general). - GatewayIntentBits.GuildMessages, + // Required to receive messages in guilds (e.g., in #general). + GatewayIntentBits.GuildMessages, - // A PRIVILEGED INTENT, required to read the content of messages. - // This is necessary for the AI handler, admin commands, and "quoi/feur". - GatewayIntentBits.MessageContent, + // A PRIVILEGED INTENT, required to read the content of messages. + // This is necessary for the AI handler, admin commands, and "quoi/feur". + GatewayIntentBits.MessageContent, - // Required to receive updates when members join, leave, or are updated. - // Crucial for fetching member details for commands like /timeout or /info. - GatewayIntentBits.GuildMembers, + // Required to receive updates when members join, leave, or are updated. + // Crucial for fetching member details for commands like /timeout or /info. + GatewayIntentBits.GuildMembers, - // Required to receive member presence updates (online, idle, offline). - // Necessary for features like `getOnlineUsersWithRole`. - GatewayIntentBits.GuildPresences, - ], -}); \ No newline at end of file + // Required to receive member presence updates (online, idle, offline). + // Necessary for features like `getOnlineUsersWithRole`. + GatewayIntentBits.GuildPresences, + ], +}); diff --git a/src/bot/commands/floposite.js b/src/bot/commands/floposite.js index d640d0d..0f4db3a 100644 --- a/src/bot/commands/floposite.js +++ b/src/bot/commands/floposite.js @@ -1,8 +1,4 @@ -import { - InteractionResponseType, - MessageComponentTypes, - ButtonStyleTypes, -} from 'discord-interactions'; +import { InteractionResponseType, MessageComponentTypes, ButtonStyleTypes } from "discord-interactions"; /** * Handles the /floposite slash command. @@ -11,45 +7,45 @@ import { * @param {object} res - The Express response object. */ export async function handleFlopoSiteCommand(req, res) { - // The URL for the link button. Consider moving to .env if it changes. - const siteUrl = process.env.FLOPOSITE_URL || 'https://floposite.com'; + // The URL for the link button. Consider moving to .env if it changes. + const siteUrl = process.env.FLOPOSITE_URL || "https://floposite.com"; - // The URL for the thumbnail image. - const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`; + // The URL for the thumbnail image. + const thumbnailUrl = `${process.env.API_URL}/public/images/flopo.png`; - // Define the components (the link button) - const components = [ - { - type: MessageComponentTypes.ACTION_ROW, - components: [ - { - type: MessageComponentTypes.BUTTON, - label: 'Aller sur FlopoSite', - style: ButtonStyleTypes.LINK, - url: siteUrl, - }, - ], - }, - ]; + // Define the components (the link button) + const components = [ + { + type: MessageComponentTypes.ACTION_ROW, + components: [ + { + type: MessageComponentTypes.BUTTON, + label: "Aller sur FlopoSite", + style: ButtonStyleTypes.LINK, + url: siteUrl, + }, + ], + }, + ]; - // Define the embed message - const embeds = [ - { - title: 'FlopoSite', - description: "L'officiel et très goatesque site de FlopoBot.", - color: 0x6571F3, // A custom blue color - thumbnail: { - url: thumbnailUrl, - }, - }, - ]; + // Define the embed message + const embeds = [ + { + title: "FlopoSite", + description: "L'officiel et très goatesque site de FlopoBot.", + color: 0x6571f3, // A custom blue color + thumbnail: { + url: thumbnailUrl, + }, + }, + ]; - // Send the response to Discord - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: embeds, - components: components, - }, - }); -} \ No newline at end of file + // Send the response to Discord + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: embeds, + components: components, + }, + }); +} diff --git a/src/bot/commands/info.js b/src/bot/commands/info.js index b125ecd..a20e729 100644 --- a/src/bot/commands/info.js +++ b/src/bot/commands/info.js @@ -1,4 +1,4 @@ -import { InteractionResponseType } from 'discord-interactions'; +import { InteractionResponseType } from "discord-interactions"; /** * Handles the /info slash command. @@ -8,64 +8,61 @@ import { InteractionResponseType } from 'discord-interactions'; * @param {object} client - The Discord.js client instance. */ export async function handleInfoCommand(req, res, client) { - const { guild_id } = req.body; + const { guild_id } = req.body; - try { - // Fetch the guild object from the client - const guild = await client.guilds.fetch(guild_id); + try { + // Fetch the guild object from the client + const guild = await client.guilds.fetch(guild_id); - // Fetch all members to ensure the cache is up to date - await guild.members.fetch(); + // Fetch all members to ensure the cache is up to date + await guild.members.fetch(); - // Filter the cached members to find those who are timed out - // A member is timed out if their `communicationDisabledUntil` property is a future date. - const timedOutMembers = guild.members.cache.filter( - (member) => - member.communicationDisabledUntilTimestamp && - member.communicationDisabledUntilTimestamp > Date.now() - ); + // Filter the cached members to find those who are timed out + // A member is timed out if their `communicationDisabledUntil` property is a future date. + const timedOutMembers = guild.members.cache.filter( + (member) => member.communicationDisabledUntilTimestamp && member.communicationDisabledUntilTimestamp > Date.now(), + ); - // --- Case 1: No members are timed out --- - if (timedOutMembers.size === 0) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [ - { - title: 'Membres Timeout', - description: "Aucun membre n'est actuellement timeout.", - color: 0x4F545C, // Discord's gray color - }, - ], - }, - }); - } + // --- Case 1: No members are timed out --- + if (timedOutMembers.size === 0) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + title: "Membres Timeout", + description: "Aucun membre n'est actuellement timeout.", + color: 0x4f545c, // Discord's gray color + }, + ], + }, + }); + } - // --- Case 2: At least one member is timed out --- - // Format the list of timed-out members for the embed - const memberList = timedOutMembers - .map((member) => { - // toLocaleString provides a user-friendly date and time format - const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString('fr-FR'); - return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`; - }) - .join('\n'); + // --- Case 2: At least one member is timed out --- + // Format the list of timed-out members for the embed + const memberList = timedOutMembers + .map((member) => { + // toLocaleString provides a user-friendly date and time format + const expiration = new Date(member.communicationDisabledUntilTimestamp).toLocaleString("fr-FR"); + return `▫️ **${member.user.globalName || member.user.username}** (jusqu'au ${expiration})`; + }) + .join("\n"); - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [ - { - title: 'Membres Actuellement Timeout', - description: memberList, - color: 0xED4245, // Discord's red color - }, - ], - }, - }); - - } catch (error) { - console.error('Error handling /info command:', error); - return res.status(500).json({ error: 'Failed to fetch timeout information.' }); - } -} \ No newline at end of file + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + title: "Membres Actuellement Timeout", + description: memberList, + color: 0xed4245, // Discord's red color + }, + ], + }, + }); + } catch (error) { + console.error("Error handling /info command:", error); + return res.status(500).json({ error: "Failed to fetch timeout information." }); + } +} diff --git a/src/bot/commands/inventory.js b/src/bot/commands/inventory.js index 3a5eec2..9fd272f 100644 --- a/src/bot/commands/inventory.js +++ b/src/bot/commands/inventory.js @@ -1,11 +1,11 @@ import { - InteractionResponseType, - MessageComponentTypes, - ButtonStyleTypes, - InteractionResponseFlags, -} from 'discord-interactions'; -import { activeInventories, skins } from '../../game/state.js'; -import { getUserInventory } from '../../database/index.js'; + InteractionResponseType, + MessageComponentTypes, + ButtonStyleTypes, + InteractionResponseFlags, +} from "discord-interactions"; +import { activeInventories, skins } from "../../game/state.js"; +import { getUserInventory } from "../../database/index.js"; /** * Handles the /inventory slash command. @@ -17,122 +17,149 @@ import { getUserInventory } from '../../database/index.js'; * @param {string} interactionId - The unique ID of the interaction. */ export async function handleInventoryCommand(req, res, client, interactionId) { - const { member, guild_id, token, data } = req.body; - const commandUserId = member.user.id; - // User can specify another member, otherwise it defaults to themself - const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId; + const { member, guild_id, token, data } = req.body; + const commandUserId = member.user.id; + // User can specify another member, otherwise it defaults to themself + const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId; - try { - // --- 1. Fetch Data --- - const guild = await client.guilds.fetch(guild_id); - const targetMember = await guild.members.fetch(targetUserId); - const inventorySkins = getUserInventory.all({ user_id: targetUserId }); + try { + // --- 1. Fetch Data --- + const guild = await client.guilds.fetch(guild_id); + const targetMember = await guild.members.fetch(targetUserId); + const inventorySkins = getUserInventory.all({ user_id: targetUserId }); - // --- 2. Handle Empty Inventory --- - if (inventorySkins.length === 0) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [{ - title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, - description: "Cet inventaire est vide.", - color: 0x4F545C, // Discord Gray - }], - }, - }); - } + // --- 2. Handle Empty Inventory --- + if (inventorySkins.length === 0) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, + description: "Cet inventaire est vide.", + color: 0x4f545c, // Discord Gray + }, + ], + }, + }); + } - // --- 3. Store Interactive Session State --- - // This state is crucial for the component handlers to know which inventory to update. - activeInventories[interactionId] = { - akhyId: targetUserId, // The inventory owner - userId: commandUserId, // The user who ran the command - page: 0, - amount: inventorySkins.length, - endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`, - timestamp: Date.now(), - inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn - }; + // --- 3. Store Interactive Session State --- + // This state is crucial for the component handlers to know which inventory to update. + activeInventories[interactionId] = { + akhyId: targetUserId, // The inventory owner + userId: commandUserId, // The user who ran the command + page: 0, + amount: inventorySkins.length, + endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`, + timestamp: Date.now(), + inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn + }; - // --- 4. Prepare Embed Content --- - const currentSkin = inventorySkins[0]; - const skinData = skins.find((s) => s.uuid === currentSkin.uuid); - if (!skinData) { - throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); - } - const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0); + // --- 4. Prepare Embed Content --- + const currentSkin = inventorySkins[0]; + const skinData = skins.find((s) => s.uuid === currentSkin.uuid); + if (!skinData) { + throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); + } + const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0); - // --- Helper functions for formatting --- - const getChromaText = (skin, skinInfo) => { - let result = ""; - for (let i = 1; i <= skinInfo.chromas.length; i++) { - result += skin.currentChroma === i ? '💠 ' : '◾ '; - } - return result || 'N/A'; - }; + // --- Helper functions for formatting --- + const getChromaText = (skin, skinInfo) => { + let result = ""; + for (let i = 1; i <= skinInfo.chromas.length; i++) { + result += skin.currentChroma === i ? "💠 " : "◾ "; + } + return result || "N/A"; + }; - const getChromaName = (skin, skinInfo) => { - if (skin.currentChroma > 1) { - const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName.replace(/[\r\n]+/g, ' ').replace(skinInfo.displayName, '').trim(); - const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); - return match ? match[1].trim() : name; - } - return 'Base'; - }; + const getChromaName = (skin, skinInfo) => { + if (skin.currentChroma > 1) { + const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName + .replace(/[\r\n]+/g, " ") + .replace(skinInfo.displayName, "") + .trim(); + const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); + return match ? match[1].trim() : name; + } + return "Base"; + }; - const getImageUrl = (skin, skinInfo) => { - if (skin.currentLvl === skinInfo.levels.length) { - const chroma = skinInfo.chromas[skin.currentChroma - 1]; - return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon; - } - const level = skinInfo.levels[skin.currentLvl - 1]; - return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender; - }; + const getImageUrl = (skin, skinInfo) => { + if (skin.currentLvl === skinInfo.levels.length) { + const chroma = skinInfo.chromas[skin.currentChroma - 1]; + return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon; + } + const level = skinInfo.levels[skin.currentLvl - 1]; + return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender; + }; - // --- 5. Build Initial Components (Buttons) --- - const components = [ - { type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY }, - { type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY }, - ]; + // --- 5. Build Initial Components (Buttons) --- + const components = [ + { + type: MessageComponentTypes.BUTTON, + custom_id: `prev_page_${interactionId}`, + label: "⏮️ Préc.", + style: ButtonStyleTypes.SECONDARY, + }, + { + type: MessageComponentTypes.BUTTON, + custom_id: `next_page_${interactionId}`, + label: "Suiv. ⏭️", + style: ButtonStyleTypes.SECONDARY, + }, + ]; - const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length; - // Only show upgrade button if the skin is upgradable AND the command user owns the inventory - if (isUpgradable && targetUserId === commandUserId) { - components.push({ - type: MessageComponentTypes.BUTTON, - custom_id: `upgrade_${interactionId}`, - label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`, - style: ButtonStyleTypes.PRIMARY, - }); - } + const isUpgradable = + currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length; + // Only show upgrade button if the skin is upgradable AND the command user owns the inventory + if (isUpgradable && targetUserId === commandUserId) { + components.push({ + type: MessageComponentTypes.BUTTON, + custom_id: `upgrade_${interactionId}`, + label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`, + style: ButtonStyleTypes.PRIMARY, + }); + } - // --- 6. Send Final Response --- - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [{ - title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, - color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, - footer: { text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` }, - fields: [{ - name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, - value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, - }], - image: { url: getImageUrl(currentSkin, skinData) }, - }], - components: [{ type: MessageComponentTypes.ACTION_ROW, components: components }, - { type: MessageComponentTypes.ACTION_ROW, - components: [{ - type: MessageComponentTypes.BUTTON, - url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`, - label: 'Voir sur FlopoSite', - style: ButtonStyleTypes.LINK,}] - }], - }, - }); - - } catch (error) { - console.error('Error handling /inventory command:', error); - return res.status(500).json({ error: 'Failed to generate inventory.' }); - } -} \ No newline at end of file + // --- 6. Send Final Response --- + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, + color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3, + footer: { + text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`, + }, + fields: [ + { + name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, + value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, + }, + ], + image: { url: getImageUrl(currentSkin, skinData) }, + }, + ], + components: [ + { type: MessageComponentTypes.ACTION_ROW, components: components }, + { + type: MessageComponentTypes.ACTION_ROW, + components: [ + { + type: MessageComponentTypes.BUTTON, + url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`, + label: "Voir sur FlopoSite", + style: ButtonStyleTypes.LINK, + }, + ], + }, + ], + }, + }); + } catch (error) { + console.error("Error handling /inventory command:", error); + return res.status(500).json({ error: "Failed to generate inventory." }); + } +} diff --git a/src/bot/commands/search.js b/src/bot/commands/search.js index dd70c84..e9a4b84 100644 --- a/src/bot/commands/search.js +++ b/src/bot/commands/search.js @@ -1,11 +1,11 @@ import { - InteractionResponseType, - InteractionResponseFlags, - MessageComponentTypes, - ButtonStyleTypes, -} from 'discord-interactions'; -import { activeSearchs, skins } from '../../game/state.js'; -import { getAllSkins } from '../../database/index.js'; + InteractionResponseType, + InteractionResponseFlags, + MessageComponentTypes, + ButtonStyleTypes, +} from "discord-interactions"; +import { activeSearchs, skins } from "../../game/state.js"; +import { getAllSkins } from "../../database/index.js"; /** * Handles the /search slash command. @@ -17,106 +17,117 @@ import { getAllSkins } from '../../database/index.js'; * @param {string} interactionId - The unique ID of the interaction. */ export async function handleSearchCommand(req, res, client, interactionId) { - const { member, guild_id, token, data } = req.body; - const userId = member.user.id; - const searchValue = data.options[0].value.toLowerCase(); + const { member, guild_id, token, data } = req.body; + const userId = member.user.id; + const searchValue = data.options[0].value.toLowerCase(); - try { - // --- 1. Fetch and Filter Data --- - const allDbSkins = getAllSkins.all(); - const resultSkins = allDbSkins.filter((skin) => - skin.displayName.toLowerCase().includes(searchValue) || - skin.tierText.toLowerCase().includes(searchValue) - ); + try { + // --- 1. Fetch and Filter Data --- + const allDbSkins = getAllSkins.all(); + const resultSkins = allDbSkins.filter( + (skin) => + skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue), + ); - // --- 2. Handle No Results --- - if (resultSkins.length === 0) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'Aucun skin ne correspond à votre recherche.', - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // --- 2. Handle No Results --- + if (resultSkins.length === 0) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Aucun skin ne correspond à votre recherche.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // --- 3. Store Interactive Session State --- - activeSearchs[interactionId] = { - userId: userId, - page: 0, - amount: resultSkins.length, - resultSkins: resultSkins, - endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`, - timestamp: Date.now(), - searchValue: searchValue, - }; + // --- 3. Store Interactive Session State --- + activeSearchs[interactionId] = { + userId: userId, + page: 0, + amount: resultSkins.length, + resultSkins: resultSkins, + endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`, + timestamp: Date.now(), + searchValue: searchValue, + }; - // --- 4. Prepare Initial Embed Content --- - const guild = await client.guilds.fetch(guild_id); - const currentSkin = resultSkins[0]; - const skinData = skins.find((s) => s.uuid === currentSkin.uuid); - if (!skinData) { - throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); - } + // --- 4. Prepare Initial Embed Content --- + const guild = await client.guilds.fetch(guild_id); + const currentSkin = resultSkins[0]; + const skinData = skins.find((s) => s.uuid === currentSkin.uuid); + if (!skinData) { + throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); + } - // Fetch owner details if the skin is owned - let ownerText = ''; - if (currentSkin.user_id) { - try { - const owner = await guild.members.fetch(currentSkin.user_id); - ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`; - } catch (e) { - console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); - ownerText = '| Appartenant à un utilisateur inconnu'; - } - } + // Fetch owner details if the skin is owned + let ownerText = ""; + if (currentSkin.user_id) { + try { + const owner = await guild.members.fetch(currentSkin.user_id); + ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`; + } catch (e) { + console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); + ownerText = "| Appartenant à un utilisateur inconnu"; + } + } - // Helper to get the best possible image for the skin - const getImageUrl = (skinInfo) => { - const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1]; - if (lastChroma?.fullRender) return lastChroma.fullRender; - if (lastChroma?.displayIcon) return lastChroma.displayIcon; + // Helper to get the best possible image for the skin + const getImageUrl = (skinInfo) => { + const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1]; + if (lastChroma?.fullRender) return lastChroma.fullRender; + if (lastChroma?.displayIcon) return lastChroma.displayIcon; - const lastLevel = skinInfo.levels[skinInfo.levels.length - 1]; - if (lastLevel?.displayIcon) return lastLevel.displayIcon; + const lastLevel = skinInfo.levels[skinInfo.levels.length - 1]; + if (lastLevel?.displayIcon) return lastLevel.displayIcon; - return skinInfo.displayIcon; // Fallback to base icon - }; + return skinInfo.displayIcon; // Fallback to base icon + }; - // --- 5. Build Initial Components & Embed --- - const components = [ - { - type: MessageComponentTypes.ACTION_ROW, - components: [ - { type: MessageComponentTypes.BUTTON, custom_id: `prev_search_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY }, - { type: MessageComponentTypes.BUTTON, custom_id: `next_search_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY }, - ], - }, - ]; + // --- 5. Build Initial Components & Embed --- + const components = [ + { + type: MessageComponentTypes.ACTION_ROW, + components: [ + { + type: MessageComponentTypes.BUTTON, + custom_id: `prev_search_page_${interactionId}`, + label: "⏮️ Préc.", + style: ButtonStyleTypes.SECONDARY, + }, + { + type: MessageComponentTypes.BUTTON, + custom_id: `next_search_page_${interactionId}`, + label: "Suiv. ⏭️", + style: ButtonStyleTypes.SECONDARY, + }, + ], + }, + ]; - const embed = { - title: 'Résultats de la recherche', - description: `🔎 _"${searchValue}"_`, - color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, - fields: [{ - name: `**${currentSkin.displayName}**`, - value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`, - }], - image: { url: getImageUrl(skinData) }, - footer: { text: `Résultat 1/${resultSkins.length}` }, - }; + const embed = { + title: "Résultats de la recherche", + description: `🔎 _"${searchValue}"_`, + color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3, + fields: [ + { + name: `**${currentSkin.displayName}**`, + value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`, + }, + ], + image: { url: getImageUrl(skinData) }, + footer: { text: `Résultat 1/${resultSkins.length}` }, + }; - // --- 6. Send Final Response --- - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [embed], - components: components, - }, - }); - - } catch (error) { - console.error('Error handling /search command:', error); - return res.status(500).json({ error: 'Failed to execute search.' }); - } -} \ No newline at end of file + // --- 6. Send Final Response --- + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [embed], + components: components, + }, + }); + } catch (error) { + console.error("Error handling /search command:", error); + return res.status(500).json({ error: "Failed to execute search." }); + } +} diff --git a/src/bot/commands/skins.js b/src/bot/commands/skins.js index 5ce4ec1..5b568c6 100644 --- a/src/bot/commands/skins.js +++ b/src/bot/commands/skins.js @@ -1,5 +1,5 @@ -import { InteractionResponseType } from 'discord-interactions'; -import { getTopSkins } from '../../database/index.js'; +import { InteractionResponseType } from "discord-interactions"; +import { getTopSkins } from "../../database/index.js"; /** * Handles the /skins slash command. @@ -9,60 +9,59 @@ import { getTopSkins } from '../../database/index.js'; * @param {object} client - The Discord.js client instance. */ export async function handleSkinsCommand(req, res, client) { - const { guild_id } = req.body; + const { guild_id } = req.body; - try { - // --- 1. Fetch Data --- - const topSkins = getTopSkins.all(); - const guild = await client.guilds.fetch(guild_id); - const fields = []; + try { + // --- 1. Fetch Data --- + const topSkins = getTopSkins.all(); + const guild = await client.guilds.fetch(guild_id); + const fields = []; - // --- 2. Build Embed Fields Asynchronously --- - // We use a for...of loop to handle the async fetch for each owner. - for (const [index, skin] of topSkins.entries()) { - let ownerText = 'Libre'; // Default text if the skin has no owner + // --- 2. Build Embed Fields Asynchronously --- + // We use a for...of loop to handle the async fetch for each owner. + for (const [index, skin] of topSkins.entries()) { + let ownerText = "Libre"; // Default text if the skin has no owner - // If the skin has an owner, fetch their details - if (skin.user_id) { - try { - const owner = await guild.members.fetch(skin.user_id); - // Use globalName if available, otherwise fallback to username - ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`; - } catch (e) { - // This can happen if the user has left the server - console.warn(`Could not fetch owner for user ID: ${skin.user_id}`); - ownerText = 'Appartient à un utilisateur inconnu'; - } - } + // If the skin has an owner, fetch their details + if (skin.user_id) { + try { + const owner = await guild.members.fetch(skin.user_id); + // Use globalName if available, otherwise fallback to username + ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`; + } catch (e) { + // This can happen if the user has left the server + console.warn(`Could not fetch owner for user ID: ${skin.user_id}`); + ownerText = "Appartient à un utilisateur inconnu"; + } + } - // Add the formatted skin info to our fields array - fields.push({ - name: `#${index + 1} - **${skin.displayName}**`, - value: `Valeur Max: **${skin.maxPrice} Flopos** | ${ownerText}`, - inline: false, - }); - } + // Add the formatted skin info to our fields array + fields.push({ + name: `#${index + 1} - **${skin.displayName}**`, + value: `Valeur Max: **${skin.maxPrice} Flopos** | ${ownerText}`, + inline: false, + }); + } - // --- 3. Send the Response --- - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [ - { - title: '🏆 Top 10 des Skins les Plus Chers', - description: 'Classement des skins par leur valeur maximale potentielle.', - fields: fields, - color: 0xFFD700, // Gold color for a leaderboard - footer: { - text: 'Utilisez /inventory pour voir vos propres skins.' - } - }, - ], - }, - }); - - } catch (error) { - console.error('Error handling /skins command:', error); - return res.status(500).json({ error: 'Failed to fetch the skins leaderboard.' }); - } -} \ No newline at end of file + // --- 3. Send the Response --- + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + title: "🏆 Top 10 des Skins les Plus Chers", + description: "Classement des skins par leur valeur maximale potentielle.", + fields: fields, + color: 0xffd700, // Gold color for a leaderboard + footer: { + text: "Utilisez /inventory pour voir vos propres skins.", + }, + }, + ], + }, + }); + } catch (error) { + console.error("Error handling /skins command:", error); + return res.status(500).json({ error: "Failed to fetch the skins leaderboard." }); + } +} diff --git a/src/bot/commands/timeout.js b/src/bot/commands/timeout.js index 1fe11f5..f9ad3ef 100644 --- a/src/bot/commands/timeout.js +++ b/src/bot/commands/timeout.js @@ -1,15 +1,15 @@ import { - InteractionResponseType, - InteractionResponseFlags, - MessageComponentTypes, - ButtonStyleTypes, -} from 'discord-interactions'; + InteractionResponseType, + InteractionResponseFlags, + MessageComponentTypes, + ButtonStyleTypes, +} from "discord-interactions"; -import { formatTime, getOnlineUsersWithRole } from '../../utils/index.js'; -import { DiscordRequest } from '../../api/discord.js'; -import { activePolls } from '../../game/state.js'; -import { getSocketIo } from '../../server/socket.js'; -import { getUser } from '../../database/index.js'; +import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js"; +import { DiscordRequest } from "../../api/discord.js"; +import { activePolls } from "../../game/state.js"; +import { getSocketIo } from "../../server/socket.js"; +import { getUser } from "../../database/index.js"; /** * Handles the /timeout slash command. @@ -18,193 +18,225 @@ import { getUser } from '../../database/index.js'; * @param {object} client - The Discord.js client instance. */ export async function handleTimeoutCommand(req, res, client) { - const io = getSocketIo(); - const { id, member, guild_id, channel_id, token, data } = req.body; - const { options } = data; + const io = getSocketIo(); + const { id, member, guild_id, channel_id, token, data } = req.body; + const { options } = data; - // Extract command options - const userId = member.user.id; - const targetUserId = options[0].value; - const time = options[1].value; + // Extract command options + const userId = member.user.id; + const targetUserId = options[0].value; + const time = options[1].value; - // Fetch member objects from Discord - const guild = await client.guilds.fetch(guild_id); - const fromMember = await guild.members.fetch(userId); - const toMember = await guild.members.fetch(targetUserId); + // Fetch member objects from Discord + const guild = await client.guilds.fetch(guild_id); + const fromMember = await guild.members.fetch(userId); + const toMember = await guild.members.fetch(targetUserId); - // --- Validation Checks --- - // 1. Check if a poll is already running for the target user - const existingPoll = Object.values(activePolls).find(poll => poll.toUserId === targetUserId); - if (existingPoll) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `Impossible de lancer un vote pour **${toMember.user.globalName}**, un vote est déjà en cours.`, - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // --- Validation Checks --- + // 1. Check if a poll is already running for the target user + const existingPoll = Object.values(activePolls).find((poll) => poll.toUserId === targetUserId); + if (existingPoll) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `Impossible de lancer un vote pour **${toMember.user.globalName}**, un vote est déjà en cours.`, + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // 2. Check if the user is already timed out - if (toMember.communicationDisabledUntilTimestamp > Date.now()) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `**${toMember.user.globalName}** est déjà timeout.`, - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // 2. Check if the user is already timed out + if (toMember.communicationDisabledUntilTimestamp > Date.now()) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `**${toMember.user.globalName}** est déjà timeout.`, + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // --- Poll Initialization --- - const pollId = id; // Use the interaction ID as the unique poll ID - const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; + // --- Poll Initialization --- + const pollId = id; // Use the interaction ID as the unique poll ID + const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; - // Calculate required votes - const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID); - const requiredMajority = Math.max( - parseInt(process.env.MIN_VOTES, 10), - Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1 - ); + // Calculate required votes + const onlineEligibleUsers = await getOnlineUsersWithRole(guild, process.env.VOTING_ROLE_ID); + const requiredMajority = Math.max( + parseInt(process.env.MIN_VOTES, 10), + Math.floor(onlineEligibleUsers.size / (time >= 21600 ? 2 : 3)) + 1, + ); - // Store poll data in the active state - activePolls[pollId] = { - id: userId, - username: fromMember.user.globalName, - toUserId: targetUserId, - toUsername: toMember.user.globalName, - time: time, - time_display: formatTime(time), - for: 0, - against: 0, - voters: [], - channelId: channel_id, - endpoint: webhookEndpoint, - endTime: Date.now() + parseInt(process.env.POLL_TIME, 10) * 1000, - requiredMajority: requiredMajority, - }; + // Store poll data in the active state + activePolls[pollId] = { + id: userId, + username: fromMember.user.globalName, + toUserId: targetUserId, + toUsername: toMember.user.globalName, + time: time, + time_display: formatTime(time), + for: 0, + against: 0, + voters: [], + channelId: channel_id, + endpoint: webhookEndpoint, + endTime: Date.now() + parseInt(process.env.POLL_TIME, 10) * 1000, + requiredMajority: requiredMajority, + }; - // --- Set up Countdown Interval --- - const countdownInterval = setInterval(async () => { - const poll = activePolls[pollId]; + // --- Set up Countdown Interval --- + const countdownInterval = setInterval(async () => { + const poll = activePolls[pollId]; - // If poll no longer exists, clear the interval - if (!poll) { - clearInterval(countdownInterval); - return; - } + // If poll no longer exists, clear the interval + if (!poll) { + clearInterval(countdownInterval); + return; + } - const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000)); - const votesNeeded = Math.max(0, poll.requiredMajority - poll.for); - const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`; + const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000)); + const votesNeeded = Math.max(0, poll.requiredMajority - poll.for); + const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`; - // --- Poll Expiration Logic --- - if (remaining === 0) { - clearInterval(countdownInterval); + // --- Poll Expiration Logic --- + if (remaining === 0) { + clearInterval(countdownInterval); - const votersList = poll.voters.map(voterId => { - const user = getUser.get(voterId); - return `- ${user?.globalName || 'Utilisateur Inconnu'}`; - }).join('\n'); + const votersList = poll.voters + .map((voterId) => { + const user = getUser.get(voterId); + return `- ${user?.globalName || "Utilisateur Inconnu"}`; + }) + .join("\n"); - try { - await DiscordRequest(poll.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`, - description: `Il manquait **${votesNeeded}** vote(s).`, - fields: [{ - name: 'Pour', - value: `✅ ${poll.for}\n${votersList}`, - inline: true, - }], - color: 0xFF4444, // Red for failure - }], - components: [], // Remove buttons - }, - }); - } catch (err) { - console.error('Error updating failed poll message:', err); - } + try { + await DiscordRequest(poll.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: `Le vote pour timeout ${poll.toUsername} a échoué 😔`, + description: `Il manquait **${votesNeeded}** vote(s).`, + fields: [ + { + name: "Pour", + value: `✅ ${poll.for}\n${votersList}`, + inline: true, + }, + ], + color: 0xff4444, // Red for failure + }, + ], + components: [], // Remove buttons + }, + }); + } catch (err) { + console.error("Error updating failed poll message:", err); + } - // Clean up the poll from active state - delete activePolls[pollId]; - io.emit('poll-update'); // Notify frontend - return; - } + // Clean up the poll from active state + delete activePolls[pollId]; + io.emit("poll-update"); // Notify frontend + return; + } - // --- Periodic Update Logic --- - // Update the message every second with the new countdown - try { - const votersList = poll.voters.map(voterId => { - const user = getUser.get(voterId); - return `- ${user?.globalName || 'Utilisateur Inconnu'}`; - }).join('\n'); + // --- Periodic Update Logic --- + // Update the message every second with the new countdown + try { + const votersList = poll.voters + .map((voterId) => { + const user = getUser.get(voterId); + return `- ${user?.globalName || "Utilisateur Inconnu"}`; + }) + .join("\n"); - await DiscordRequest(poll.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: 'Vote de Timeout', - description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`, - fields: [{ - name: 'Pour', - value: `✅ ${poll.for}\n${votersList}`, - inline: true, - }, { - name: 'Temps restant', - value: `⏳ ${countdownText}`, - inline: false, - }], - color: 0x5865F2, // Discord Blurple - }], - // Keep the components so people can still vote - components: [{ - type: MessageComponentTypes.ACTION_ROW, - components: [ - { type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS }, - ], - }], - }, - }); - } catch (err) { - console.error('Error updating countdown:', err); - // If the message was deleted, stop trying to update it. - if (err.message.includes('Unknown Message')) { - clearInterval(countdownInterval); - delete activePolls[pollId]; - io.emit('poll-update'); - } - } - }, 2000); // Update every 2 seconds to avoid rate limits + await DiscordRequest(poll.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: "Vote de Timeout", + description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`, + fields: [ + { + name: "Pour", + value: `✅ ${poll.for}\n${votersList}`, + inline: true, + }, + { + name: "Temps restant", + value: `⏳ ${countdownText}`, + inline: false, + }, + ], + color: 0x5865f2, // Discord Blurple + }, + ], + // Keep the components so people can still vote + components: [ + { + type: MessageComponentTypes.ACTION_ROW, + components: [ + { + type: MessageComponentTypes.BUTTON, + custom_id: `vote_for_${pollId}`, + label: "Oui ✅", + style: ButtonStyleTypes.SUCCESS, + }, + ], + }, + ], + }, + }); + } catch (err) { + console.error("Error updating countdown:", err); + // If the message was deleted, stop trying to update it. + if (err.message.includes("Unknown Message")) { + clearInterval(countdownInterval); + delete activePolls[pollId]; + io.emit("poll-update"); + } + } + }, 2000); // Update every 2 seconds to avoid rate limits - // --- Send Initial Response --- - io.emit('poll-update'); // Notify frontend + // --- Send Initial Response --- + io.emit("poll-update"); // Notify frontend - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - embeds: [{ - title: 'Vote de Timeout', - description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`, - fields: [{ - name: 'Pour', - value: '✅ 0', - inline: true, - }, { - name: 'Temps restant', - value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`, - inline: false, - }], - color: 0x5865F2, - }], - components: [{ - type: MessageComponentTypes.ACTION_ROW, - components: [ - { type: MessageComponentTypes.BUTTON, custom_id: `vote_for_${pollId}`, label: 'Oui ✅', style: ButtonStyleTypes.SUCCESS }, - ], - }], - }, - }); -} \ No newline at end of file + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + embeds: [ + { + title: "Vote de Timeout", + description: `**${activePolls[pollId].username}** propose de timeout **${activePolls[pollId].toUsername}** pendant ${activePolls[pollId].time_display}.\nIl manque **${activePolls[pollId].requiredMajority}** vote(s).`, + fields: [ + { + name: "Pour", + value: "✅ 0", + inline: true, + }, + { + name: "Temps restant", + value: `⏳ **${Math.floor((activePolls[pollId].endTime - Date.now()) / 60000)}m**`, + inline: false, + }, + ], + color: 0x5865f2, + }, + ], + components: [ + { + type: MessageComponentTypes.ACTION_ROW, + components: [ + { + type: MessageComponentTypes.BUTTON, + custom_id: `vote_for_${pollId}`, + label: "Oui ✅", + style: ButtonStyleTypes.SUCCESS, + }, + ], + }, + ], + }, + }); +} diff --git a/src/bot/commands/valorant.js b/src/bot/commands/valorant.js index 7dd842a..748b4e5 100644 --- a/src/bot/commands/valorant.js +++ b/src/bot/commands/valorant.js @@ -1,13 +1,10 @@ -import { - InteractionResponseType, - InteractionResponseFlags, -} from 'discord-interactions'; -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions"; +import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import { postAPOBuy } from '../../utils/index.js'; -import { DiscordRequest } from '../../api/discord.js'; -import {getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js'; -import { skins } from '../../game/state.js'; +import { postAPOBuy } from "../../utils/index.js"; +import { DiscordRequest } from "../../api/discord.js"; +import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js"; +import { skins } from "../../game/state.js"; /** * Handles the /valorant slash command for opening a "skin case". @@ -17,198 +14,201 @@ import { skins } from '../../game/state.js'; * @param {object} client - The Discord.js client instance. */ export async function handleValorantCommand(req, res, client) { - const { member, token } = req.body; - const userId = member.user.id; - const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500; + const { member, token } = req.body; + const userId = member.user.id; + const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500; - try { - // --- 1. Verify and process payment --- + try { + // --- 1. Verify and process payment --- - const commandUser = getUser.get(userId); - if (!commandUser) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Erreur lors de la récupération de votre profil utilisateur.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } - if (commandUser.coins < valoPrice) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `Pas assez de FlopoCoins (${valoPrice} requis).`, - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + const commandUser = getUser.get(userId); + if (!commandUser) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Erreur lors de la récupération de votre profil utilisateur.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } + if (commandUser.coins < valoPrice) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `Pas assez de FlopoCoins (${valoPrice} requis).`, + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - insertLog.run({ - id: `${userId}-${Date.now()}`, - user_id: userId, - action: 'VALO_CASE_OPEN', - target_user_id: null, - coins_amount: -valoPrice, - user_new_amount: commandUser.coins - valoPrice, - }); - updateUserCoins.run({ - id: userId, - coins: commandUser.coins - valoPrice, - }) + insertLog.run({ + id: `${userId}-${Date.now()}`, + user_id: userId, + action: "VALO_CASE_OPEN", + target_user_id: null, + coins_amount: -valoPrice, + user_new_amount: commandUser.coins - valoPrice, + }); + updateUserCoins.run({ + id: userId, + coins: commandUser.coins - valoPrice, + }); - // --- 2. Send Initial "Opening" Response --- - // Acknowledge the interaction immediately with a loading message. - const initialEmbed = new EmbedBuilder() - .setTitle('Ouverture de la caisse...') - .setImage('https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif') - .setColor('#F2F3F3'); + // --- 2. Send Initial "Opening" Response --- + // Acknowledge the interaction immediately with a loading message. + const initialEmbed = new EmbedBuilder() + .setTitle("Ouverture de la caisse...") + .setImage("https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif") + .setColor("#F2F3F3"); - await res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { embeds: [initialEmbed] }, - }); + await res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { embeds: [initialEmbed] }, + }); + // --- 3. Run the skin reveal logic after a delay --- + setTimeout(async () => { + const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; + try { + // --- Skin Selection --- + const availableSkins = getAllAvailableSkins.all(); + if (availableSkins.length === 0) { + throw new Error("No available skins to award."); + } + const dbSkin = availableSkins[Math.floor(Math.random() * availableSkins.length)]; + const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); + if (!randomSkinData) { + throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); + } - // --- 3. Run the skin reveal logic after a delay --- - setTimeout(async () => { - const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; - try { - // --- Skin Selection --- - const availableSkins = getAllAvailableSkins.all(); - if (availableSkins.length === 0) { - throw new Error("No available skins to award."); - } - const dbSkin = availableSkins[Math.floor(Math.random() * availableSkins.length)]; - const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); - if (!randomSkinData) { - throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); - } + // --- Randomize Level and Chroma --- + const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; + let randomChroma = 1; + if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { + // Ensure chroma is at least 1 and not greater than the number of chromas + randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; + } - // --- Randomize Level and Chroma --- - const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; - let randomChroma = 1; - if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { - // Ensure chroma is at least 1 and not greater than the number of chromas - randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; - } + // --- Calculate Price --- + const calculatePrice = () => { + let result = parseFloat(dbSkin.basePrice); + result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2); + result *= 1 + randomChroma / 4; + return parseFloat(result.toFixed(0)); + }; + const finalPrice = calculatePrice(); - // --- Calculate Price --- - const calculatePrice = () => { - let result = parseFloat(dbSkin.basePrice); - result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2))); - result *= (1 + (randomChroma / 4)); - return parseFloat(result.toFixed(0)); - }; - const finalPrice = calculatePrice(); + // --- Update Database --- + await updateSkin.run({ + uuid: randomSkinData.uuid, + user_id: userId, + currentLvl: randomLevel, + currentChroma: randomChroma, + currentPrice: finalPrice, + }); - // --- Update Database --- - await updateSkin.run({ - uuid: randomSkinData.uuid, - user_id: userId, - currentLvl: randomLevel, - currentChroma: randomChroma, - currentPrice: finalPrice, - }); + // --- Prepare Final Embed and Components --- + const finalEmbed = buildFinalEmbed(dbSkin, randomSkinData, randomLevel, randomChroma, finalPrice); + const components = buildComponents(randomSkinData, randomLevel, randomChroma); - // --- Prepare Final Embed and Components --- - const finalEmbed = buildFinalEmbed(dbSkin, randomSkinData, randomLevel, randomChroma, finalPrice); - const components = buildComponents(randomSkinData, randomLevel, randomChroma); - - // --- Edit the Original Message with the Result --- - await DiscordRequest(webhookEndpoint, { - method: 'PATCH', - body: { - embeds: [finalEmbed], - components: components, - }, - }); - - } catch (revealError) { - console.error('Error during skin reveal:', revealError); - // Inform the user that something went wrong - await DiscordRequest(webhookEndpoint, { - method: 'PATCH', - body: { - content: "Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.", - embeds: [], - }, - }); - } - }, 5000); // 5-second delay for suspense - - } catch (error) { - console.error('Error handling /valorant command:', error); - // This catches errors from the initial interaction, e.g., the payment API call. - return res.status(500).json({ error: 'Failed to initiate the case opening.' }); - } + // --- Edit the Original Message with the Result --- + await DiscordRequest(webhookEndpoint, { + method: "PATCH", + body: { + embeds: [finalEmbed], + components: components, + }, + }); + } catch (revealError) { + console.error("Error during skin reveal:", revealError); + // Inform the user that something went wrong + await DiscordRequest(webhookEndpoint, { + method: "PATCH", + body: { + content: + "Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.", + embeds: [], + }, + }); + } + }, 5000); // 5-second delay for suspense + } catch (error) { + console.error("Error handling /valorant command:", error); + // This catches errors from the initial interaction, e.g., the payment API call. + return res.status(500).json({ error: "Failed to initiate the case opening." }); + } } // --- Helper Functions --- /** Builds the final embed to display the won skin. */ function buildFinalEmbed(dbSkin, skinData, level, chroma, price) { - const selectedChromaData = skinData.chromas[chroma - 1] || {}; + const selectedChromaData = skinData.chromas[chroma - 1] || {}; - const getChromaName = () => { - if (chroma > 1) { - const name = selectedChromaData.displayName?.replace(/[\r\n]+/g, ' ').replace(skinData.displayName, '').trim(); - const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); - return match ? match[1].trim() : (name || 'Chroma Inconnu'); - } - return 'Base'; - }; + const getChromaName = () => { + if (chroma > 1) { + const name = selectedChromaData.displayName + ?.replace(/[\r\n]+/g, " ") + .replace(skinData.displayName, "") + .trim(); + const match = name?.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); + return match ? match[1].trim() : name || "Chroma Inconnu"; + } + return "Base"; + }; - const getImageUrl = () => { - if (level === skinData.levels.length) { - return selectedChromaData.fullRender || selectedChromaData.displayIcon || skinData.displayIcon; - } - const levelData = skinData.levels[level - 1]; - return levelData?.displayIcon || skinData.displayIcon; - }; + const getImageUrl = () => { + if (level === skinData.levels.length) { + return selectedChromaData.fullRender || selectedChromaData.displayIcon || skinData.displayIcon; + } + const levelData = skinData.levels[level - 1]; + return levelData?.displayIcon || skinData.displayIcon; + }; - const lvlText = (level >= 1 ? '1️⃣' : '') + - (level >= 2 ? '2️⃣' : '') + - (level >= 3 ? '3️⃣' : '') + - (level >= 4 ? '4️⃣' : '') + - (level >= 5 ? '5️⃣' : '') + - (level >= 6 ? '6️⃣' : '') + - '◾'.repeat(skinData.levels.length - level); - const chromaText = '💠'.repeat(chroma) + '◾'.repeat(skinData.chromas.length - chroma); + const lvlText = + (level >= 1 ? "1️⃣" : "") + + (level >= 2 ? "2️⃣" : "") + + (level >= 3 ? "3️⃣" : "") + + (level >= 4 ? "4️⃣" : "") + + (level >= 5 ? "5️⃣" : "") + + (level >= 6 ? "6️⃣" : "") + + "◾".repeat(skinData.levels.length - level); + const chromaText = "💠".repeat(chroma) + "◾".repeat(skinData.chromas.length - chroma); - return new EmbedBuilder() - .setTitle(`${skinData.displayName} | ${getChromaName()}`) - .setDescription(dbSkin.tierText) - .setColor(`#${dbSkin.tierColor}`) - .setImage(getImageUrl()) - .setFields([ - { name: 'Lvl', value: lvlText || 'N/A', inline: true }, - { name: 'Chroma', value: chromaText || 'N/A', inline: true }, - { name: 'Prix', value: `**${price}** <:vp:1362964205808128122>`, inline: true }, - ]) - .setFooter({ text: 'Skin ajouté à votre inventaire !' }); + return new EmbedBuilder() + .setTitle(`${skinData.displayName} | ${getChromaName()}`) + .setDescription(dbSkin.tierText) + .setColor(`#${dbSkin.tierColor}`) + .setImage(getImageUrl()) + .setFields([ + { name: "Lvl", value: lvlText || "N/A", inline: true }, + { name: "Chroma", value: chromaText || "N/A", inline: true }, + { + name: "Prix", + value: `**${price}** <:vp:1362964205808128122>`, + inline: true, + }, + ]) + .setFooter({ text: "Skin ajouté à votre inventaire !" }); } /** Builds the action row with a video button if a video is available. */ function buildComponents(skinData, level, chroma) { - const selectedLevelData = skinData.levels[level - 1] || {}; - const selectedChromaData = skinData.chromas[chroma - 1] || {}; + const selectedLevelData = skinData.levels[level - 1] || {}; + const selectedChromaData = skinData.chromas[chroma - 1] || {}; - let videoUrl = null; - if (level === skinData.levels.length) { - videoUrl = selectedChromaData.streamedVideo; - } - videoUrl = videoUrl || selectedLevelData.streamedVideo; + let videoUrl = null; + if (level === skinData.levels.length) { + videoUrl = selectedChromaData.streamedVideo; + } + videoUrl = videoUrl || selectedLevelData.streamedVideo; - if (videoUrl) { - return [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('🎬 Aperçu Vidéo') - .setStyle(ButtonStyle.Link) - .setURL(videoUrl) - ) - ]; - } - return []; // Return an empty array if no video is available -} \ No newline at end of file + if (videoUrl) { + return [ + new ActionRowBuilder().addComponents( + new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl), + ), + ]; + } + return []; // Return an empty array if no video is available +} diff --git a/src/bot/components/inventoryNav.js b/src/bot/components/inventoryNav.js index fae3831..d1e2910 100644 --- a/src/bot/components/inventoryNav.js +++ b/src/bot/components/inventoryNav.js @@ -1,12 +1,12 @@ import { - InteractionResponseType, - MessageComponentTypes, - ButtonStyleTypes, - InteractionResponseFlags, -} from 'discord-interactions'; + InteractionResponseType, + MessageComponentTypes, + ButtonStyleTypes, + InteractionResponseFlags, +} from "discord-interactions"; -import { DiscordRequest } from '../../api/discord.js'; -import { activeInventories, skins } from '../../game/state.js'; +import { DiscordRequest } from "../../api/discord.js"; +import { activeInventories, skins } from "../../game/state.js"; /** * Handles navigation button clicks (Previous/Next) for the inventory embed. @@ -15,144 +15,167 @@ import { activeInventories, skins } from '../../game/state.js'; * @param {object} client - The Discord.js client instance. */ export async function handleInventoryNav(req, res, client) { - const { member, data, guild_id } = req.body; - const { custom_id } = data; + const { member, data, guild_id } = req.body; + const { custom_id } = data; - // Extract direction ('prev' or 'next') and the original interaction ID from the custom_id - const [direction, page, interactionId] = custom_id.split('_'); + // Extract direction ('prev' or 'next') and the original interaction ID from the custom_id + const [direction, page, interactionId] = custom_id.split("_"); - // --- 1. Retrieve the interactive session --- - const inventorySession = activeInventories[interactionId]; + // --- 1. Retrieve the interactive session --- + const inventorySession = activeInventories[interactionId]; - // --- 2. Validation Checks --- - if (!inventorySession) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Oups, cet affichage d'inventaire a expiré. Veuillez relancer la commande `/inventory`.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // --- 2. Validation Checks --- + if (!inventorySession) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Oups, cet affichage d'inventaire a expiré. Veuillez relancer la commande `/inventory`.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // Ensure the user clicking the button is the one who initiated the command - if (inventorySession.userId !== member.user.id) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Vous ne pouvez pas naviguer dans l'inventaire de quelqu'un d'autre.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // Ensure the user clicking the button is the one who initiated the command + if (inventorySession.userId !== member.user.id) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Vous ne pouvez pas naviguer dans l'inventaire de quelqu'un d'autre.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } + // --- 3. Update Page Number --- + const { amount } = inventorySession; + if (direction === "next") { + inventorySession.page = (inventorySession.page + 1) % amount; + } else if (direction === "prev") { + inventorySession.page = (inventorySession.page - 1 + amount) % amount; + } - // --- 3. Update Page Number --- - const { amount } = inventorySession; - if (direction === 'next') { - inventorySession.page = (inventorySession.page + 1) % amount; - } else if (direction === 'prev') { - inventorySession.page = (inventorySession.page - 1 + amount) % amount; - } + try { + // --- 4. Rebuild Embed with New Page Content --- + const { page, inventorySkins } = inventorySession; + const currentSkin = inventorySkins[page]; + const skinData = skins.find((s) => s.uuid === currentSkin.uuid); + if (!skinData) { + throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); + } + const guild = await client.guilds.fetch(guild_id); + const targetMember = await guild.members.fetch(inventorySession.akhyId); + const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0); - try { - // --- 4. Rebuild Embed with New Page Content --- - const { page, inventorySkins } = inventorySession; - const currentSkin = inventorySkins[page]; - const skinData = skins.find((s) => s.uuid === currentSkin.uuid); - if (!skinData) { - throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); - } + // --- Helper functions for formatting --- + const getChromaText = (skin, skinInfo) => { + let result = ""; + for (let i = 1; i <= skinInfo.chromas.length; i++) { + result += skin.currentChroma === i ? "💠 " : "◾ "; + } + return result || "N/A"; + }; - const guild = await client.guilds.fetch(guild_id); - const targetMember = await guild.members.fetch(inventorySession.akhyId); - const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0); + const getChromaName = (skin, skinInfo) => { + if (skin.currentChroma > 1) { + const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName + .replace(/[\r\n]+/g, " ") + .replace(skinInfo.displayName, "") + .trim(); + const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); + return match ? match[1].trim() : name; + } + return "Base"; + }; - // --- Helper functions for formatting --- - const getChromaText = (skin, skinInfo) => { - let result = ""; - for (let i = 1; i <= skinInfo.chromas.length; i++) { - result += skin.currentChroma === i ? '💠 ' : '◾ '; - } - return result || 'N/A'; - }; + const getImageUrl = (skin, skinInfo) => { + if (skin.currentLvl === skinInfo.levels.length) { + const chroma = skinInfo.chromas[skin.currentChroma - 1]; + return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon; + } + const level = skinInfo.levels[skin.currentLvl - 1]; + return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender; + }; - const getChromaName = (skin, skinInfo) => { - if (skin.currentChroma > 1) { - const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName.replace(/[\r\n]+/g, ' ').replace(skinInfo.displayName, '').trim(); - const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i); - return match ? match[1].trim() : name; - } - return 'Base'; - }; + // --- 5. Rebuild Components (Buttons) --- + let components = [ + { + type: MessageComponentTypes.BUTTON, + custom_id: `prev_page_${interactionId}`, + label: "⏮️ Préc.", + style: ButtonStyleTypes.SECONDARY, + }, + { + type: MessageComponentTypes.BUTTON, + custom_id: `next_page_${interactionId}`, + label: "Suiv. ⏭️", + style: ButtonStyleTypes.SECONDARY, + }, + ]; - const getImageUrl = (skin, skinInfo) => { - if (skin.currentLvl === skinInfo.levels.length) { - const chroma = skinInfo.chromas[skin.currentChroma - 1]; - return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon; - } - const level = skinInfo.levels[skin.currentLvl - 1]; - return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender; - }; + const isUpgradable = + currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length; + // Conditionally add the upgrade button + if (isUpgradable && inventorySession.akhyId === inventorySession.userId) { + components.push({ + type: MessageComponentTypes.BUTTON, + custom_id: `upgrade_${interactionId}`, + label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`, + style: ButtonStyleTypes.PRIMARY, + }); + } - // --- 5. Rebuild Components (Buttons) --- - let components = [ - { type: MessageComponentTypes.BUTTON, custom_id: `prev_page_${interactionId}`, label: '⏮️ Préc.', style: ButtonStyleTypes.SECONDARY }, - { type: MessageComponentTypes.BUTTON, custom_id: `next_page_${interactionId}`, label: 'Suiv. ⏭️', style: ButtonStyleTypes.SECONDARY }, - ]; + // --- 6. Send PATCH Request to Update the Message --- + await DiscordRequest(inventorySession.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, + color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3, + footer: { + text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`, + }, + fields: [ + { + name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, + value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, + }, + ], + image: { url: getImageUrl(currentSkin, skinData) }, + }, + ], + components: [ + { type: MessageComponentTypes.ACTION_ROW, components: components }, + { + type: MessageComponentTypes.ACTION_ROW, + components: [ + { + type: MessageComponentTypes.BUTTON, + url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`, + label: "Voir sur FlopoSite", + style: ButtonStyleTypes.LINK, + }, + ], + }, + ], + }, + }); - const isUpgradable = currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length; - // Conditionally add the upgrade button - if (isUpgradable && inventorySession.akhyId === inventorySession.userId) { - components.push({ - type: MessageComponentTypes.BUTTON, - custom_id: `upgrade_${interactionId}`, - label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice/10).toFixed(0)} Flopos)`, - style: ButtonStyleTypes.PRIMARY, - }); - } - - // --- 6. Send PATCH Request to Update the Message --- - await DiscordRequest(inventorySession.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`, - color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, - footer: { text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos` }, - fields: [{ - name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`, - value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`, - }], - image: { url: getImageUrl(currentSkin, skinData) }, - }], - components: [{ type: MessageComponentTypes.ACTION_ROW, components: components }, - { type: MessageComponentTypes.ACTION_ROW, - components: [{ - type: MessageComponentTypes.BUTTON, - url: `${process.env.FLAPI_URL}/akhy/${targetMember.id}`, - label: 'Voir sur FlopoSite', - style: ButtonStyleTypes.LINK,}] - }], - }, - }); - - // --- 7. Acknowledge the Interaction --- - // This tells Discord the interaction was received, and since the message is already updated, - // no further action is needed. - return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); - - } catch (error) { - console.error('Error handling inventory navigation:', error); - // In case of an error, we should still acknowledge the interaction to prevent it from failing. - // We can send a silent, ephemeral error message. - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'Une erreur est survenue lors de la mise à jour de l\'inventaire.', - flags: InteractionResponseFlags.EPHEMERAL, - } - }); - } -} \ No newline at end of file + // --- 7. Acknowledge the Interaction --- + // This tells Discord the interaction was received, and since the message is already updated, + // no further action is needed. + return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); + } catch (error) { + console.error("Error handling inventory navigation:", error); + // In case of an error, we should still acknowledge the interaction to prevent it from failing. + // We can send a silent, ephemeral error message. + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Une erreur est survenue lors de la mise à jour de l'inventaire.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } +} diff --git a/src/bot/components/pollVote.js b/src/bot/components/pollVote.js index d7cdc0e..12bef1a 100644 --- a/src/bot/components/pollVote.js +++ b/src/bot/components/pollVote.js @@ -1,11 +1,8 @@ -import { - InteractionResponseType, - InteractionResponseFlags, -} from 'discord-interactions'; -import { DiscordRequest } from '../../api/discord.js'; -import { activePolls } from '../../game/state.js'; -import { getSocketIo } from '../../server/socket.js'; -import { getUser } from '../../database/index.js'; +import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions"; +import { DiscordRequest } from "../../api/discord.js"; +import { activePolls } from "../../game/state.js"; +import { getSocketIo } from "../../server/socket.js"; +import { getUser } from "../../database/index.js"; /** * Handles clicks on the 'Yes' or 'No' buttons of a timeout poll. @@ -13,164 +10,175 @@ import { getUser } from '../../database/index.js'; * @param {object} res - The Express response object. */ export async function handlePollVote(req, res) { - const io = getSocketIo(); - const { member, data, guild_id } = req.body; - const { custom_id } = data; + const io = getSocketIo(); + const { member, data, guild_id } = req.body; + const { custom_id } = data; - // --- 1. Parse Component ID --- - const [_, voteType, pollId] = custom_id.split('_'); // e.g., ['vote', 'for', '12345...'] - const isVotingFor = voteType === 'for'; + // --- 1. Parse Component ID --- + const [_, voteType, pollId] = custom_id.split("_"); // e.g., ['vote', 'for', '12345...'] + const isVotingFor = voteType === "for"; - // --- 2. Retrieve Poll and Validate --- - const poll = activePolls[pollId]; - const voterId = member.user.id; + // --- 2. Retrieve Poll and Validate --- + const poll = activePolls[pollId]; + const voterId = member.user.id; - if (!poll) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Ce sondage de timeout n'est plus actif.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + if (!poll) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Ce sondage de timeout n'est plus actif.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // Check if the voter has the required role - if (!member.roles.includes(process.env.VOTING_ROLE_ID)) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Vous n'avez pas le rôle requis pour participer à ce vote.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // Check if the voter has the required role + if (!member.roles.includes(process.env.VOTING_ROLE_ID)) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Vous n'avez pas le rôle requis pour participer à ce vote.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // Prevent user from voting on themselves - if (poll.toUserId === voterId) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Vous ne pouvez pas voter pour vous-même.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // Prevent user from voting on themselves + if (poll.toUserId === voterId) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Vous ne pouvez pas voter pour vous-même.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // Prevent double voting - if (poll.voters.includes(voterId)) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'Vous avez déjà voté pour ce sondage.', - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // Prevent double voting + if (poll.voters.includes(voterId)) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Vous avez déjà voté pour ce sondage.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // --- 3. Record the Vote --- - poll.voters.push(voterId); - if (isVotingFor) { - poll.for++; - } else { - poll.against++; - } + // --- 3. Record the Vote --- + poll.voters.push(voterId); + if (isVotingFor) { + poll.for++; + } else { + poll.against++; + } - io.emit('poll-update'); // Notify frontend clients of the change + io.emit("poll-update"); // Notify frontend clients of the change - const votersList = poll.voters.map(vId => `- ${getUser.get(vId)?.globalName || 'Utilisateur Inconnu'}`).join('\n'); + const votersList = poll.voters.map((vId) => `- ${getUser.get(vId)?.globalName || "Utilisateur Inconnu"}`).join("\n"); + // --- 4. Check for Majority --- + if (isVotingFor && poll.for >= poll.requiredMajority) { + // --- SUCCESS CASE: MAJORITY REACHED --- - // --- 4. Check for Majority --- - if (isVotingFor && poll.for >= poll.requiredMajority) { - // --- SUCCESS CASE: MAJORITY REACHED --- + // a. Update the poll message to show success + try { + await DiscordRequest(poll.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: "Vote Terminé - Timeout Appliqué !", + description: `La majorité a été atteinte. **${poll.toUsername}** a été timeout pendant ${poll.time_display}.`, + fields: [ + { + name: "Votes Pour", + value: `✅ ${poll.for}\n${votersList}`, + inline: true, + }, + ], + color: 0x22a55b, // Green for success + }, + ], + components: [], // Remove buttons + }, + }); + } catch (err) { + console.error("Error updating final poll message:", err); + } - // a. Update the poll message to show success - try { - await DiscordRequest(poll.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: 'Vote Terminé - Timeout Appliqué !', - description: `La majorité a été atteinte. **${poll.toUsername}** a été timeout pendant ${poll.time_display}.`, - fields: [{ name: 'Votes Pour', value: `✅ ${poll.for}\n${votersList}`, inline: true }], - color: 0x22A55B, // Green for success - }], - components: [], // Remove buttons - }, - }); - } catch (err) { - console.error('Error updating final poll message:', err); - } + // b. Execute the timeout via Discord API + try { + const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString(); + const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`; + await DiscordRequest(endpointTimeout, { + method: "PATCH", + body: { communication_disabled_until: timeoutUntil }, + }); - // b. Execute the timeout via Discord API - try { - const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString(); - const endpointTimeout = `guilds/${guild_id}/members/${poll.toUserId}`; - await DiscordRequest(endpointTimeout, { - method: 'PATCH', - body: { communication_disabled_until: timeoutUntil }, - }); + // c. Send a public confirmation message and clean up + delete activePolls[pollId]; + io.emit("poll-update"); + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`, + }, + }); + } catch (err) { + console.error("Error timing out user:", err); + delete activePolls[pollId]; + io.emit("poll-update"); + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `La majorité a été atteinte, mais une erreur est survenue lors de l'application du timeout sur <@${poll.toUserId}>.`, + }, + }); + } + } else { + // --- PENDING CASE: NO MAJORITY YET --- - // c. Send a public confirmation message and clean up - delete activePolls[pollId]; - io.emit('poll-update'); - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `💥 <@${poll.toUserId}> a été timeout pendant **${poll.time_display}** par décision démocratique !`, - }, - }); + // a. Send an ephemeral acknowledgment to the voter + res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Votre vote a été enregistré ! ✅", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); - } catch (err) { - console.error('Error timing out user:', err); - delete activePolls[pollId]; - io.emit('poll-update'); - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `La majorité a été atteinte, mais une erreur est survenue lors de l'application du timeout sur <@${poll.toUserId}>.`, - }, - }); - } - } else { - // --- PENDING CASE: NO MAJORITY YET --- + // b. Update the original poll message asynchronously (no need to await) + // The main countdown interval will also handle this, but this provides a faster update. + const votesNeeded = Math.max(0, poll.requiredMajority - poll.for); + const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000)); + const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`; - // a. Send an ephemeral acknowledgment to the voter - res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'Votre vote a été enregistré ! ✅', - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - - // b. Update the original poll message asynchronously (no need to await) - // The main countdown interval will also handle this, but this provides a faster update. - const votesNeeded = Math.max(0, poll.requiredMajority - poll.for); - const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000)); - const countdownText = `**${Math.floor(remaining / 60)}m ${remaining % 60}s** restantes`; - - DiscordRequest(poll.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: 'Vote de Timeout', - description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`, - fields: [{ - name: 'Pour', - value: `✅ ${poll.for}\n${votersList}`, - inline: true, - }, { - name: 'Temps restant', - value: `⏳ ${countdownText}`, - inline: false, - }], - color: 0x5865F2, - }], - // Keep the original components so people can still vote - components: req.body.message.components, - }, - }).catch(err => console.error("Error updating poll after vote:", err)); - } -} \ No newline at end of file + DiscordRequest(poll.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: "Vote de Timeout", + description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}.\nIl manque **${votesNeeded}** vote(s).`, + fields: [ + { + name: "Pour", + value: `✅ ${poll.for}\n${votersList}`, + inline: true, + }, + { + name: "Temps restant", + value: `⏳ ${countdownText}`, + inline: false, + }, + ], + color: 0x5865f2, + }, + ], + // Keep the original components so people can still vote + components: req.body.message.components, + }, + }).catch((err) => console.error("Error updating poll after vote:", err)); + } +} diff --git a/src/bot/components/searchNav.js b/src/bot/components/searchNav.js index 8cbd209..afb71e7 100644 --- a/src/bot/components/searchNav.js +++ b/src/bot/components/searchNav.js @@ -1,12 +1,12 @@ import { - InteractionResponseType, - InteractionResponseFlags, - MessageComponentTypes, - ButtonStyleTypes, -} from 'discord-interactions'; + InteractionResponseType, + InteractionResponseFlags, + MessageComponentTypes, + ButtonStyleTypes, +} from "discord-interactions"; -import { DiscordRequest } from '../../api/discord.js'; -import { activeSearchs, skins } from '../../game/state.js'; +import { DiscordRequest } from "../../api/discord.js"; +import { activeSearchs, skins } from "../../game/state.js"; /** * Handles navigation button clicks (Previous/Next) for the search results embed. @@ -15,107 +15,110 @@ import { activeSearchs, skins } from '../../game/state.js'; * @param {object} client - The Discord.js client instance. */ export async function handleSearchNav(req, res, client) { - const { member, data, guild_id } = req.body; - const { custom_id } = data; + const { member, data, guild_id } = req.body; + const { custom_id } = data; - // Extract direction and the original interaction ID from the custom_id - const [direction, _, page, interactionId] = custom_id.split('_'); // e.g., ['next', 'search', 'page', '123...'] + // Extract direction and the original interaction ID from the custom_id + const [direction, _, page, interactionId] = custom_id.split("_"); // e.g., ['next', 'search', 'page', '123...'] - // --- 1. Retrieve the interactive session --- - const searchSession = activeSearchs[interactionId]; + // --- 1. Retrieve the interactive session --- + const searchSession = activeSearchs[interactionId]; - // --- 2. Validation Checks --- - if (!searchSession) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Oups, cette recherche a expiré. Veuillez relancer la commande `/search`.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // --- 2. Validation Checks --- + if (!searchSession) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Oups, cette recherche a expiré. Veuillez relancer la commande `/search`.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // Ensure the user clicking the button is the one who initiated the command - if (searchSession.userId !== member.user.id) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Vous ne pouvez pas naviguer dans les résultats de recherche de quelqu'un d'autre.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + // Ensure the user clicking the button is the one who initiated the command + if (searchSession.userId !== member.user.id) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Vous ne pouvez pas naviguer dans les résultats de recherche de quelqu'un d'autre.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // --- 3. Update Page Number --- - const { amount } = searchSession; - if (direction === 'next') { - searchSession.page = (searchSession.page + 1) % amount; - } else if (direction === 'prev') { - searchSession.page = (searchSession.page - 1 + amount) % amount; - } + // --- 3. Update Page Number --- + const { amount } = searchSession; + if (direction === "next") { + searchSession.page = (searchSession.page + 1) % amount; + } else if (direction === "prev") { + searchSession.page = (searchSession.page - 1 + amount) % amount; + } - try { - // --- 4. Rebuild Embed with New Page Content --- - const { page, resultSkins, searchValue } = searchSession; - const currentSkin = resultSkins[page]; - const skinData = skins.find((s) => s.uuid === currentSkin.uuid); - if (!skinData) { - throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); - } + try { + // --- 4. Rebuild Embed with New Page Content --- + const { page, resultSkins, searchValue } = searchSession; + const currentSkin = resultSkins[page]; + const skinData = skins.find((s) => s.uuid === currentSkin.uuid); + if (!skinData) { + throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`); + } - // Fetch owner details if the skin is owned - let ownerText = ''; - if (currentSkin.user_id) { - try { - const owner = await client.users.fetch(currentSkin.user_id); - ownerText = `| **@${owner.globalName || owner.username}** ✅`; - } catch (e) { - console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); - ownerText = '| Appartenant à un utilisateur inconnu'; - } - } + // Fetch owner details if the skin is owned + let ownerText = ""; + if (currentSkin.user_id) { + try { + const owner = await client.users.fetch(currentSkin.user_id); + ownerText = `| **@${owner.globalName || owner.username}** ✅`; + } catch (e) { + console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); + ownerText = "| Appartenant à un utilisateur inconnu"; + } + } - // Helper to get the best possible image for the skin - const getImageUrl = (skinInfo) => { - const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1]; - if (lastChroma?.fullRender) return lastChroma.fullRender; - if (lastChroma?.displayIcon) return lastChroma.displayIcon; - const lastLevel = skinInfo.levels[skinInfo.levels.length - 1]; - if (lastLevel?.displayIcon) return lastLevel.displayIcon; - return skinInfo.displayIcon; - }; + // Helper to get the best possible image for the skin + const getImageUrl = (skinInfo) => { + const lastChroma = skinInfo.chromas[skinInfo.chromas.length - 1]; + if (lastChroma?.fullRender) return lastChroma.fullRender; + if (lastChroma?.displayIcon) return lastChroma.displayIcon; + const lastLevel = skinInfo.levels[skinInfo.levels.length - 1]; + if (lastLevel?.displayIcon) return lastLevel.displayIcon; + return skinInfo.displayIcon; + }; - // --- 5. Send PATCH Request to Update the Message --- - // Note: The components (buttons) do not change, so we can reuse them from the original message. - await DiscordRequest(searchSession.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: 'Résultats de la recherche', - description: `🔎 _"${searchValue}"_`, - color: parseInt(currentSkin.tierColor, 16) || 0xF2F3F3, - fields: [{ - name: `**${currentSkin.displayName}**`, - value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`, - }], - image: { url: getImageUrl(skinData) }, - footer: { text: `Résultat ${page + 1}/${amount}` }, - }], - components: req.body.message.components, // Reuse existing components - }, - }); + // --- 5. Send PATCH Request to Update the Message --- + // Note: The components (buttons) do not change, so we can reuse them from the original message. + await DiscordRequest(searchSession.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: "Résultats de la recherche", + description: `🔎 _"${searchValue}"_`, + color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3, + fields: [ + { + name: `**${currentSkin.displayName}**`, + value: `${currentSkin.tierText}\nValeur Max: **${currentSkin.maxPrice} Flopos** ${ownerText}`, + }, + ], + image: { url: getImageUrl(skinData) }, + footer: { text: `Résultat ${page + 1}/${amount}` }, + }, + ], + components: req.body.message.components, // Reuse existing components + }, + }); - // --- 6. Acknowledge the Interaction --- - return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); - - } catch (error) { - console.error('Error handling search navigation:', error); - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: 'Une erreur est survenue lors de la mise à jour de la recherche.', - flags: InteractionResponseFlags.EPHEMERAL, - } - }); - } -} \ No newline at end of file + // --- 6. Acknowledge the Interaction --- + return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); + } catch (error) { + console.error("Error handling search navigation:", error); + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Une erreur est survenue lors de la mise à jour de la recherche.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } +} diff --git a/src/bot/components/upgradeSkin.js b/src/bot/components/upgradeSkin.js index a695a04..ff03cf0 100644 --- a/src/bot/components/upgradeSkin.js +++ b/src/bot/components/upgradeSkin.js @@ -1,15 +1,15 @@ import { - InteractionResponseType, - InteractionResponseFlags, - MessageComponentTypes, - ButtonStyleTypes, -} from 'discord-interactions'; -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; + InteractionResponseType, + InteractionResponseFlags, + MessageComponentTypes, + ButtonStyleTypes, +} from "discord-interactions"; +import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; -import { DiscordRequest } from '../../api/discord.js'; -import { postAPOBuy } from '../../utils/index.js'; -import { activeInventories, skins } from '../../game/state.js'; -import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../database/index.js'; +import { DiscordRequest } from "../../api/discord.js"; +import { postAPOBuy } from "../../utils/index.js"; +import { activeInventories, skins } from "../../game/state.js"; +import { getSkin, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js"; /** * Handles the click of the 'Upgrade' button on a skin in the inventory. @@ -17,202 +17,228 @@ import {getSkin, getUser, insertLog, updateSkin, updateUserCoins} from '../../da * @param {object} res - The Express response object. */ export async function handleUpgradeSkin(req, res) { - const { member, data } = req.body; - const { custom_id } = data; + const { member, data } = req.body; + const { custom_id } = data; - const interactionId = custom_id.replace('upgrade_', ''); - const userId = member.user.id; + const interactionId = custom_id.replace("upgrade_", ""); + const userId = member.user.id; - // --- 1. Retrieve Session and Validate --- - const inventorySession = activeInventories[interactionId]; - if (!inventorySession) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { content: "Cet affichage d'inventaire a expiré.", flags: InteractionResponseFlags.EPHEMERAL }, - }); - } + // --- 1. Retrieve Session and Validate --- + const inventorySession = activeInventories[interactionId]; + if (!inventorySession) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Cet affichage d'inventaire a expiré.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - // Ensure the user clicking is the inventory owner - if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.", flags: InteractionResponseFlags.EPHEMERAL }, - }); - } + // Ensure the user clicking is the inventory owner + if (inventorySession.akhyId !== userId || inventorySession.userId !== userId) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Vous ne pouvez pas améliorer un skin qui ne vous appartient pas.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page]; - const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid); + const skinToUpgrade = inventorySession.inventorySkins[inventorySession.page]; + const skinData = skins.find((s) => s.uuid === skinToUpgrade.uuid); - if (!skinData || (skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length)) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.", flags: InteractionResponseFlags.EPHEMERAL }, - }); - } + if ( + !skinData || + (skinToUpgrade.currentLvl >= skinData.levels.length && skinToUpgrade.currentChroma >= skinData.chromas.length) + ) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Ce skin est déjà au niveau maximum et ne peut pas être amélioré.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } + // --- 2. Handle Payment --- + const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10; - // --- 2. Handle Payment --- - const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10; + const commandUser = getUser.get(userId); - const commandUser = getUser.get(userId); + if (!commandUser) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: "Erreur lors de la récupération de votre profil utilisateur.", + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } + if (commandUser.coins < upgradePrice) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `Pas assez de FlopoCoins (${upgradePrice.toFixed(0)} requis).`, + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + } - if (!commandUser) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: "Erreur lors de la récupération de votre profil utilisateur.", - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } - if (commandUser.coins < upgradePrice) { - return res.send({ - type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, - data: { - content: `Pas assez de FlopoCoins (${upgradePrice.toFixed(0)} requis).`, - flags: InteractionResponseFlags.EPHEMERAL, - }, - }); - } + insertLog.run({ + id: `${userId}-${Date.now()}`, + user_id: userId, + action: "VALO_SKIN_UPGRADE", + target_user_id: null, + coins_amount: -upgradePrice.toFixed(0), + user_new_amount: commandUser.coins - upgradePrice.toFixed(0), + }); + updateUserCoins.run({ + id: userId, + coins: commandUser.coins - upgradePrice.toFixed(0), + }); - insertLog.run({ - id: `${userId}-${Date.now()}`, - user_id: userId, - action: 'VALO_SKIN_UPGRADE', - target_user_id: null, - coins_amount: -upgradePrice.toFixed(0), - user_new_amount: commandUser.coins - upgradePrice.toFixed(0), - }); - updateUserCoins.run({ - id: userId, - coins: commandUser.coins - upgradePrice.toFixed(0), - }) + // --- 3. Show Loading Animation --- + // Acknowledge the click immediately and then edit the message to show a loading state. + await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); + await DiscordRequest(inventorySession.endpoint, { + method: "PATCH", + body: { + embeds: [ + { + title: "Amélioration en cours...", + image: { + url: "https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif", + }, + color: 0x4f545c, + }, + ], + components: [], + }, + }); - // --- 3. Show Loading Animation --- - // Acknowledge the click immediately and then edit the message to show a loading state. - await res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE }); + // --- 4. Perform Upgrade Logic --- + let succeeded = false; + const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length; - await DiscordRequest(inventorySession.endpoint, { - method: 'PATCH', - body: { - embeds: [{ - title: 'Amélioration en cours...', - image: { url: 'https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif' }, - color: 0x4F545C, - }], - components: [], - }, - }); + if (isLevelUpgrade) { + // Upgrading Level + const successProb = + 1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5); + if (Math.random() < successProb) { + succeeded = true; + skinToUpgrade.currentLvl++; + } + } else { + // Upgrading Chroma + const successProb = + 1 - (skinToUpgrade.currentChroma / skinData.chromas.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5); + if (Math.random() < successProb) { + succeeded = true; + skinToUpgrade.currentChroma++; + } + } + // --- 5. Update Database if Successful --- + if (succeeded) { + const calculatePrice = () => { + let result = parseFloat(skinToUpgrade.basePrice); + result *= 1 + skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2); + result *= 1 + skinToUpgrade.currentChroma / 4; + return parseFloat(result.toFixed(0)); + }; + skinToUpgrade.currentPrice = calculatePrice(); - // --- 4. Perform Upgrade Logic --- - let succeeded = false; - const isLevelUpgrade = skinToUpgrade.currentLvl < skinData.levels.length; + await updateSkin.run({ + uuid: skinToUpgrade.uuid, + user_id: skinToUpgrade.user_id, + currentLvl: skinToUpgrade.currentLvl, + currentChroma: skinToUpgrade.currentChroma, + currentPrice: skinToUpgrade.currentPrice, + }); + // Update the session cache + inventorySession.inventorySkins[inventorySession.page] = skinToUpgrade; + } - if (isLevelUpgrade) { - // Upgrading Level - const successProb = 1 - (skinToUpgrade.currentLvl / skinData.levels.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5); - if (Math.random() < successProb) { - succeeded = true; - skinToUpgrade.currentLvl++; - } - } else { - // Upgrading Chroma - const successProb = 1 - (skinToUpgrade.currentChroma / skinData.chromas.length) * (parseInt(skinToUpgrade.tierRank) / 5 + 0.5); - if (Math.random() < successProb) { - succeeded = true; - skinToUpgrade.currentChroma++; - } - } + // --- 6. Send Final Result --- + setTimeout(async () => { + // Fetch the latest state of the skin from the database + const finalSkinState = getSkin.get(skinToUpgrade.uuid); + const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData); + const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId); - // --- 5. Update Database if Successful --- - if (succeeded) { - const calculatePrice = () => { - let result = parseFloat(skinToUpgrade.basePrice); - result *= (1 + (skinToUpgrade.currentLvl / Math.max(skinData.levels.length, 2))); - result *= (1 + (skinToUpgrade.currentChroma / 4)); - return parseFloat(result.toFixed(0)); - }; - skinToUpgrade.currentPrice = calculatePrice(); - - await updateSkin.run({ - uuid: skinToUpgrade.uuid, - user_id: skinToUpgrade.user_id, - currentLvl: skinToUpgrade.currentLvl, - currentChroma: skinToUpgrade.currentChroma, - currentPrice: skinToUpgrade.currentPrice, - }); - // Update the session cache - inventorySession.inventorySkins[inventorySession.page] = skinToUpgrade; - } - - - // --- 6. Send Final Result --- - setTimeout(async () => { - // Fetch the latest state of the skin from the database - const finalSkinState = getSkin.get(skinToUpgrade.uuid); - const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData); - const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId); - - await DiscordRequest(inventorySession.endpoint, { - method: 'PATCH', - body: { - embeds: [finalEmbed], - components: finalComponents, - }, - }); - }, 2000); // Delay for the result to feel more impactful + await DiscordRequest(inventorySession.endpoint, { + method: "PATCH", + body: { + embeds: [finalEmbed], + components: finalComponents, + }, + }); + }, 2000); // Delay for the result to feel more impactful } // --- Helper Functions --- /** Builds the result embed (success or failure). */ function buildFinalEmbed(succeeded, skin, skinData) { - const embed = new EmbedBuilder() - .setTitle(succeeded ? 'Amélioration Réussie ! 🎉' : "L'amélioration a échoué... ❌") - .setDescription(`**${skin.displayName}**`) - .setImage(skin.displayIcon) // A static image is fine here - .setColor(succeeded ? 0x22A55B : 0xED4245); + const embed = new EmbedBuilder() + .setTitle(succeeded ? "Amélioration Réussie ! 🎉" : "L'amélioration a échoué... ❌") + .setDescription(`**${skin.displayName}**`) + .setImage(skin.displayIcon) // A static image is fine here + .setColor(succeeded ? 0x22a55b : 0xed4245); - if (succeeded) { - embed.addFields( - { name: 'Nouveau Niveau', value: `${skin.currentLvl}/${skinData.levels.length}`, inline: true }, - { name: 'Nouveau Chroma', value: `${skin.currentChroma}/${skinData.chromas.length}`, inline: true }, - { name: 'Nouvelle Valeur', value: `**${skin.currentPrice} Flopos**`, inline: true } - ); - } else { - embed.addFields({ name: 'Statut', value: 'Aucun changement.' }); - } - return embed; + if (succeeded) { + embed.addFields( + { + name: "Nouveau Niveau", + value: `${skin.currentLvl}/${skinData.levels.length}`, + inline: true, + }, + { + name: "Nouveau Chroma", + value: `${skin.currentChroma}/${skinData.chromas.length}`, + inline: true, + }, + { + name: "Nouvelle Valeur", + value: `**${skin.currentPrice} Flopos**`, + inline: true, + }, + ); + } else { + embed.addFields({ name: "Statut", value: "Aucun changement." }); + } + return embed; } /** Builds the result components (Retry button or Video link). */ function buildFinalComponents(succeeded, skinData, skin, interactionId) { - const isMaxed = skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length; + const isMaxed = skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length; - if (isMaxed) return []; // No buttons if maxed out + if (isMaxed) return []; // No buttons if maxed out - const row = new ActionRowBuilder(); - if (succeeded) { - // Check for video on the new level/chroma - const levelData = skinData.levels[skin.currentLvl - 1] || {}; - const chromaData = skinData.chromas[skin.currentChroma - 1] || {}; - const videoUrl = levelData.streamedVideo || chromaData.streamedVideo; + const row = new ActionRowBuilder(); + if (succeeded) { + // Check for video on the new level/chroma + const levelData = skinData.levels[skin.currentLvl - 1] || {}; + const chromaData = skinData.chromas[skin.currentChroma - 1] || {}; + const videoUrl = levelData.streamedVideo || chromaData.streamedVideo; - if (videoUrl) { - row.addComponents(new ButtonBuilder().setLabel('🎬 Aperçu Vidéo').setStyle(ButtonStyle.Link).setURL(videoUrl)); - } else { - return []; // No button if no video - } - } else { - // Add a "Retry" button - row.addComponents( - new ButtonBuilder() - .setLabel('Réessayer 🔄️') - .setStyle(ButtonStyle.Primary) - .setCustomId(`upgrade_${interactionId}`) - ); - } - return [row]; -} \ No newline at end of file + if (videoUrl) { + row.addComponents(new ButtonBuilder().setLabel("🎬 Aperçu Vidéo").setStyle(ButtonStyle.Link).setURL(videoUrl)); + } else { + return []; // No button if no video + } + } else { + // Add a "Retry" button + row.addComponents( + new ButtonBuilder() + .setLabel("Réessayer 🔄️") + .setStyle(ButtonStyle.Primary) + .setCustomId(`upgrade_${interactionId}`), + ); + } + return [row]; +} diff --git a/src/bot/events.js b/src/bot/events.js index 9c2dacb..9c0882b 100644 --- a/src/bot/events.js +++ b/src/bot/events.js @@ -1,5 +1,5 @@ -import { handleMessageCreate } from './handlers/messageCreate.js'; -import { getAkhys, setupCronJobs } from '../utils/index.js'; +import { handleMessageCreate } from "./handlers/messageCreate.js"; +import { getAkhys, setupCronJobs } from "../utils/index.js"; /** * Initializes and attaches all necessary event listeners to the Discord client. @@ -9,44 +9,44 @@ import { getAkhys, setupCronJobs } from '../utils/index.js'; * @param {object} io - The Socket.IO server instance for real-time communication. */ export function initializeEvents(client, io) { - // --- on 'ready' --- - // This event fires once the bot has successfully logged in and is ready to operate. - // It's a good place for setup tasks that require the bot to be online. - client.once('ready', async () => { - console.log(`Bot is ready and logged in as ${client.user.tag}!`); - console.log('[Startup] Bot is ready, performing initial data sync...'); - await getAkhys(client); - console.log('[Startup] Setting up scheduled tasks...'); - setupCronJobs(client, io); - console.log('--- FlopoBOT is fully operational ---'); - }); + // --- on 'ready' --- + // This event fires once the bot has successfully logged in and is ready to operate. + // It's a good place for setup tasks that require the bot to be online. + client.once("clientReady", async () => { + console.log(`Bot is ready and logged in as ${client.user.tag}!`); + console.log("[Startup] Bot is ready, performing initial data sync..."); + await getAkhys(client); + console.log("[Startup] Setting up scheduled tasks..."); + setupCronJobs(client, io); + console.log("--- FlopoBOT is fully operational ---"); + }); - // --- on 'messageCreate' --- - // This event fires every time a message is sent in a channel the bot can see. - // The logic is delegated to its own dedicated handler for cleanliness. - client.on('messageCreate', async (message) => { - // We pass the client and io instances to the handler so it has access to them - // without needing to import them, preventing potential circular dependencies. - await handleMessageCreate(message, client, io); - }); + // --- on 'messageCreate' --- + // This event fires every time a message is sent in a channel the bot can see. + // The logic is delegated to its own dedicated handler for cleanliness. + client.on("messageCreate", async (message) => { + // We pass the client and io instances to the handler so it has access to them + // without needing to import them, preventing potential circular dependencies. + await handleMessageCreate(message, client, io); + }); - // --- on 'interactionCreate' (Alternative Method) --- - // While we handle interactions via the Express endpoint for scalability and statelessness, - // you could also listen for them via the gateway like this. - // It's commented out because our current architecture uses the webhook approach. - /* + // --- on 'interactionCreate' (Alternative Method) --- + // While we handle interactions via the Express endpoint for scalability and statelessness, + // you could also listen for them via the gateway like this. + // It's commented out because our current architecture uses the webhook approach. + /* client.on('interactionCreate', async (interaction) => { // Logic to handle interactions would go here if not using a webhook endpoint. }); */ - // You can add more event listeners here as your bot's functionality grows. - // For example, listening for new members joining the server: - // client.on('guildMemberAdd', (member) => { - // console.log(`Welcome to the server, ${member.user.tag}!`); - // const welcomeChannel = member.guild.channels.cache.get('YOUR_WELCOME_CHANNEL_ID'); - // if (welcomeChannel) { - // welcomeChannel.send(`Please welcome <@${member.id}> to the server!`); - // } - // }); -} \ No newline at end of file + // You can add more event listeners here as your bot's functionality grows. + // For example, listening for new members joining the server: + // client.on('guildMemberAdd', (member) => { + // console.log(`Welcome to the server, ${member.user.tag}!`); + // const welcomeChannel = member.guild.channels.cache.get('YOUR_WELCOME_CHANNEL_ID'); + // if (welcomeChannel) { + // welcomeChannel.send(`Please welcome <@${member.id}> to the server!`); + // } + // }); +} diff --git a/src/bot/handlers/interactionCreate.js b/src/bot/handlers/interactionCreate.js index e403607..9180ff9 100644 --- a/src/bot/handlers/interactionCreate.js +++ b/src/bot/handlers/interactionCreate.js @@ -1,22 +1,19 @@ -import { - InteractionType, - InteractionResponseType, -} from 'discord-interactions'; +import { InteractionType, InteractionResponseType } from "discord-interactions"; // --- Command Handlers --- -import { handleTimeoutCommand } from '../commands/timeout.js'; -import { handleInventoryCommand } from '../commands/inventory.js'; -import { handleValorantCommand } from '../commands/valorant.js'; -import { handleInfoCommand } from '../commands/info.js'; -import { handleSkinsCommand } from '../commands/skins.js'; -import { handleSearchCommand } from '../commands/search.js'; -import { handleFlopoSiteCommand } from '../commands/floposite.js'; +import { handleTimeoutCommand } from "../commands/timeout.js"; +import { handleInventoryCommand } from "../commands/inventory.js"; +import { handleValorantCommand } from "../commands/valorant.js"; +import { handleInfoCommand } from "../commands/info.js"; +import { handleSkinsCommand } from "../commands/skins.js"; +import { handleSearchCommand } from "../commands/search.js"; +import { handleFlopoSiteCommand } from "../commands/floposite.js"; // --- Component Handlers --- -import { handlePollVote } from '../components/pollVote.js'; -import { handleInventoryNav } from '../components/inventoryNav.js'; -import { handleUpgradeSkin } from '../components/upgradeSkin.js'; -import { handleSearchNav } from '../components/searchNav.js'; +import { handlePollVote } from "../components/pollVote.js"; +import { handleInventoryNav } from "../components/inventoryNav.js"; +import { handleUpgradeSkin } from "../components/upgradeSkin.js"; +import { handleSearchNav } from "../components/searchNav.js"; /** * The main handler for all incoming interactions from Discord. @@ -25,65 +22,64 @@ import { handleSearchNav } from '../components/searchNav.js'; * @param {object} client - The Discord.js client instance. */ export async function handleInteraction(req, res, client) { - const { type, data, id } = req.body; + const { type, data, id } = req.body; - try { - if (type === InteractionType.PING) { - return res.send({ type: InteractionResponseType.PONG }); - } + try { + if (type === InteractionType.PING) { + return res.send({ type: InteractionResponseType.PONG }); + } - if (type === InteractionType.APPLICATION_COMMAND) { - const { name } = data; + if (type === InteractionType.APPLICATION_COMMAND) { + const { name } = data; - switch (name) { - case 'timeout': - return await handleTimeoutCommand(req, res, client); - case 'inventory': - return await handleInventoryCommand(req, res, client, id); - case 'valorant': - return await handleValorantCommand(req, res, client); - case 'info': - return await handleInfoCommand(req, res, client); - case 'skins': - return await handleSkinsCommand(req, res, client); - case 'search': - return await handleSearchCommand(req, res, client, id); - case 'floposite': - return await handleFlopoSiteCommand(req, res); - default: - console.error(`Unknown command: ${name}`); - return res.status(400).json({ error: 'Unknown command' }); - } - } + switch (name) { + case "timeout": + return await handleTimeoutCommand(req, res, client); + case "inventory": + return await handleInventoryCommand(req, res, client, id); + case "valorant": + return await handleValorantCommand(req, res, client); + case "info": + return await handleInfoCommand(req, res, client); + case "skins": + return await handleSkinsCommand(req, res, client); + case "search": + return await handleSearchCommand(req, res, client, id); + case "floposite": + return await handleFlopoSiteCommand(req, res); + default: + console.error(`Unknown command: ${name}`); + return res.status(400).json({ error: "Unknown command" }); + } + } - if (type === InteractionType.MESSAGE_COMPONENT) { - const componentId = data.custom_id; + if (type === InteractionType.MESSAGE_COMPONENT) { + const componentId = data.custom_id; - if (componentId.startsWith('vote_')) { - return await handlePollVote(req, res, client); - } - if (componentId.startsWith('prev_page') || componentId.startsWith('next_page')) { - return await handleInventoryNav(req, res, client); - } - if (componentId.startsWith('upgrade_')) { - return await handleUpgradeSkin(req, res, client); - } - if (componentId.startsWith('prev_search_page') || componentId.startsWith('next_search_page')) { - return await handleSearchNav(req, res, client); - } + if (componentId.startsWith("vote_")) { + return await handlePollVote(req, res, client); + } + if (componentId.startsWith("prev_page") || componentId.startsWith("next_page")) { + return await handleInventoryNav(req, res, client); + } + if (componentId.startsWith("upgrade_")) { + return await handleUpgradeSkin(req, res, client); + } + if (componentId.startsWith("prev_search_page") || componentId.startsWith("next_search_page")) { + return await handleSearchNav(req, res, client); + } - // Fallback for other potential components - console.error(`Unknown component ID: ${componentId}`); - return res.status(400).json({ error: 'Unknown component' }); - } + // Fallback for other potential components + console.error(`Unknown component ID: ${componentId}`); + return res.status(400).json({ error: "Unknown component" }); + } - // --- Fallback for Unknown Interaction Types --- - console.error('Unknown interaction type:', type); - return res.status(400).json({ error: 'Unknown interaction type' }); - - } catch (error) { - console.error('Error handling interaction:', error); - // Send a generic error response to Discord if something goes wrong - return res.status(500).json({ error: 'An internal error occurred' }); - } -} \ No newline at end of file + // --- Fallback for Unknown Interaction Types --- + console.error("Unknown interaction type:", type); + return res.status(400).json({ error: "Unknown interaction type" }); + } catch (error) { + console.error("Error handling interaction:", error); + // Send a generic error response to Discord if something goes wrong + return res.status(500).json({ error: "An internal error occurred" }); + } +} diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index 8777673..cba5b46 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -1,33 +1,36 @@ -import { sleep } from 'openai/core'; +import { sleep } from "openai/core"; import { - buildAiMessages, - buildParticipantsMap, - buildTranscript, - CONTEXT_LIMIT, - gork, INCLUDE_ATTACHMENT_URLS, MAX_ATTS_PER_MESSAGE, - stripMentionsOfBot -} from '../../utils/ai.js'; + buildAiMessages, + buildParticipantsMap, + buildTranscript, + CONTEXT_LIMIT, + gork, + INCLUDE_ATTACHMENT_URLS, + MAX_ATTS_PER_MESSAGE, + stripMentionsOfBot, +} from "../../utils/ai.js"; import { - formatTime, - postAPOBuy, - getAPOUsers, - getAkhys, - calculateBasePrice, - calculateMaxPrice -} from '../../utils/index.js'; -import { channelPointsHandler, slowmodesHandler, randomSkinPrice, initTodaysSOTD } from '../../game/points.js'; -import {requestTimestamps, activeSlowmodes, activePolls, skins, activeSolitaireGames} from '../../game/state.js'; + formatTime, + postAPOBuy, + getAPOUsers, + getAkhys, + calculateBasePrice, + calculateMaxPrice, +} from "../../utils/index.js"; +import { channelPointsHandler, slowmodesHandler, randomSkinPrice, initTodaysSOTD } from "../../game/points.js"; +import { requestTimestamps, activeSlowmodes, activePolls, skins, activeSolitaireGames } from "../../game/state.js"; import { - flopoDB, - getUser, - getAllUsers, - updateManyUsers, - insertUser, - updateUserAvatar, - getAllSkins, hardUpdateSkin -} from '../../database/index.js'; -import {client} from "../client.js"; -import {autoSolveMoves} from "../../game/solitaire.js"; + flopoDB, + getUser, + getAllUsers, + updateManyUsers, + insertUser, + updateUserAvatar, + getAllSkins, + hardUpdateSkin, +} from "../../database/index.js"; +import { client } from "../client.js"; +import { autoSolveMoves } from "../../game/solitaire.js"; // Constants for the AI rate limiter const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5"); @@ -40,250 +43,924 @@ const SPAM_INTERVAL = parseInt(process.env.SPAM_INTERVAL || "60000"); // 60 seco * @param {object} io - The Socket.IO server instance. */ export async function handleMessageCreate(message, client, io) { - // Ignore all messages from bots to prevent loops - if (message.author.bot) return; + // Ignore all messages from bots to prevent loops + if (message.author.bot) return; - // --- Specific User Gags --- - if (message.author.id === process.env.PATA_ID) { - if (message.content.toLowerCase().startsWith('feur') || message.content.toLowerCase().startsWith('rati')) { - await sleep(1000); - await message.delete().catch(console.error); - } - } + // --- Specific User Gags --- + if (message.author.id === process.env.PATA_ID) { + if (message.content.toLowerCase().startsWith("feur") || message.content.toLowerCase().startsWith("rati")) { + await sleep(1000); + await message.delete().catch(console.error); + } + } - // --- Main Guild Features (Points & Slowmode) --- - if (message.guildId === process.env.GUILD_ID) { - // Award points for activity - const pointsAwarded = await channelPointsHandler(message); - if (pointsAwarded) { - io.emit('data-updated', { table: 'users', action: 'update' }); - } + // --- Main Guild Features (Points & Slowmode) --- + if (message.guildId === process.env.GUILD_ID) { + // Award points for activity + const pointsAwarded = await channelPointsHandler(message); + if (pointsAwarded) { + io.emit("data-updated", { table: "users", action: "update" }); + } - // Enforce active slowmodes - const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes); - if (wasSlowmoded.deleted) { - io.emit('slowmode-update'); - } - } + // Enforce active slowmodes + const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes); + if (wasSlowmoded.deleted) { + io.emit("slowmode-update"); + } + } - // --- AI Mention Handler --- - if (message.mentions.has(client.user) || message.mentions.repliedUser?.id === client.user.id) { - await handleAiMention(message, client, io); - return; // Stop further processing after AI interaction - } + // --- AI Mention Handler --- + if (message.mentions.has(client.user) || message.mentions.repliedUser?.id === client.user.id) { + await handleAiMention(message, client, io); + return; // Stop further processing after AI interaction + } - // --- "Quoi/Feur" Gag --- - if (message.content.toLowerCase().includes("quoi")) { - const prob = Math.random(); - if (prob < (parseFloat(process.env.FEUR_PROB) || 0.05)) { - message.channel.send('feur').catch(console.error); - } - return; - } + // --- "Quoi/Feur" Gag --- + if (message.content.toLowerCase().includes("quoi")) { + const prob = Math.random(); + if (prob < (parseFloat(process.env.FEUR_PROB) || 0.05)) { + message.channel.send("feur").catch(console.error); + } + return; + } - // --- Admin/Dev Guild Commands --- - if (message.guildId === process.env.DEV_GUILD_ID && message.author.id === process.env.DEV_ID) { - await handleAdminCommands(message); - } + // --- Admin/Dev Guild Commands --- + if (message.guildId === process.env.DEV_GUILD_ID && message.author.id === process.env.DEV_ID) { + await handleAdminCommands(message); + } } - // --- Sub-handler for AI Logic --- async function handleAiMention(message, client, io) { - const authorId = message.author.id; - let authorDB = getUser.get(authorId); - if (!authorDB) return; // Should not happen if user is in DB, but good practice + const authorId = message.author.id; + let authorDB = getUser.get(authorId); + if (!authorDB) return; // Should not happen if user is in DB, but good practice - // --- Rate Limiting --- - const now = Date.now(); - const timestamps = (requestTimestamps.get(authorId) || []).filter(ts => now - ts < SPAM_INTERVAL); + // --- Rate Limiting --- + const now = Date.now(); + const timestamps = (requestTimestamps.get(authorId) || []).filter((ts) => now - ts < SPAM_INTERVAL); - if (timestamps.length >= MAX_REQUESTS_PER_INTERVAL) { - console.log(`Rate limit exceeded for ${authorDB.username}`); - if (!authorDB.warned) { - await message.reply(`T'abuses fréro, attends un peu ⏳`).catch(console.error); - } - // Update user's warn status - authorDB.warned = 1; - authorDB.warns += 1; - authorDB.allTimeWarns += 1; - updateManyUsers([authorDB]); + if (timestamps.length >= MAX_REQUESTS_PER_INTERVAL) { + console.log(`Rate limit exceeded for ${authorDB.username}`); + if (!authorDB.warned) { + await message.reply(`T'abuses fréro, attends un peu ⏳`).catch(console.error); + } + // Update user's warn status + authorDB.warned = 1; + authorDB.warns += 1; + authorDB.allTimeWarns += 1; + updateManyUsers([authorDB]); - // Apply timeout if warn count is too high - if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) { - try { - const member = await message.guild.members.fetch(authorId); - const time = parseInt(process.env.SPAM_TIMEOUT_TIME); - await member.timeout(time, 'Spam excessif du bot AI.'); - message.channel.send(`Ce bouffon de <@${authorId}> a été timeout pendant ${formatTime(time / 1000)}, il me cassait les couilles 🤫`).catch(console.error); - } catch (e) { - console.error('Failed to apply timeout for AI spam:', e); - message.channel.send(`<@${authorId}>, tu as de la chance que je ne puisse pas te timeout...`).catch(console.error); - } - } - return; - } + // Apply timeout if warn count is too high + if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) { + try { + const member = await message.guild.members.fetch(authorId); + const time = parseInt(process.env.SPAM_TIMEOUT_TIME); + await member.timeout(time, "Spam excessif du bot AI."); + message.channel + .send( + `Ce bouffon de <@${authorId}> a été timeout pendant ${formatTime(time / 1000)}, il me cassait les couilles 🤫`, + ) + .catch(console.error); + } catch (e) { + console.error("Failed to apply timeout for AI spam:", e); + message.channel + .send(`<@${authorId}>, tu as de la chance que je ne puisse pas te timeout...`) + .catch(console.error); + } + } + return; + } - timestamps.push(now); - requestTimestamps.set(authorId, timestamps); + timestamps.push(now); + requestTimestamps.set(authorId, timestamps); - // Reset warns if user is behaving, and increment their request count - authorDB.warned = 0; - authorDB.warns = 0; - authorDB.totalRequests += 1; - updateManyUsers([authorDB]); + // Reset warns if user is behaving, and increment their request count + authorDB.warned = 0; + authorDB.warns = 0; + authorDB.totalRequests += 1; + updateManyUsers([authorDB]); + // --- AI Processing --- + try { + await message.channel.sendTyping(); - // --- AI Processing --- - try { - await message.channel.sendTyping(); + // 1) Récup contexte + const fetched = await message.channel.messages.fetch({ + limit: Math.min(CONTEXT_LIMIT, 100), + }); + const messagesArray = Array.from(fetched.values()).reverse(); // oldest -> newest - // 1) Récup contexte - const fetched = await message.channel.messages.fetch({ limit: Math.min(CONTEXT_LIMIT, 100) }); - const messagesArray = Array.from(fetched.values()).reverse(); // oldest -> newest + const requestText = stripMentionsOfBot(message.content, client.user.id); + const invokerId = message.author.id; + const invokerName = message.member?.nickname || message.author.globalName || message.author.username; + const repliedUserId = message.mentions?.repliedUser?.id || null; - const requestText = stripMentionsOfBot(message.content, client.user.id); - const invokerId = message.author.id; - const invokerName = message.member?.nickname || message.author.globalName || message.author.username; - const repliedUserId = message.mentions?.repliedUser?.id || null; + // 2) Compact transcript & participants + const participants = buildParticipantsMap(messagesArray); + const transcript = buildTranscript(messagesArray, client.user.id); - // 2) Compact transcript & participants - const participants = buildParticipantsMap(messagesArray); - const transcript = buildTranscript(messagesArray, client.user.id); + const invokerAttachments = Array.from(message.attachments?.values?.() || []) + .slice(0, MAX_ATTS_PER_MESSAGE) + .map((a) => ({ + id: a.id, + name: a.name, + type: a.contentType || "application/octet-stream", + size: a.size, + isImage: !!(a.contentType && a.contentType.startsWith("image/")), + url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, + })); - const invokerAttachments = Array.from(message.attachments?.values?.() || []).slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({ - id: a.id, - name: a.name, - type: a.contentType || 'application/octet-stream', - size: a.size, - isImage: !!(a.contentType && a.contentType.startsWith('image/')), - url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, - })); + // 3) Construire prompts + const messageHistory = buildAiMessages({ + botId: client.user.id, + botName: "FlopoBot", + invokerId, + invokerName, + requestText, + transcript, + participants, + repliedUserId, + invokerAttachments, + }); - // 3) Construire prompts - const messageHistory = buildAiMessages({ - botId: client.user.id, - botName: 'FlopoBot', - invokerId, - invokerName, - requestText, - transcript, - participants, - repliedUserId, - invokerAttachments, - }); + // 4) Appel modèle + const reply = await gork(messageHistory); - // 4) Appel modèle - const reply = await gork(messageHistory); - - // 5) Réponse - await message.reply(reply); - - } catch (err) { - console.error("Error processing AI mention:", err); - await message.reply("Oups, mon cerveau a grillé. Réessaie plus tard.").catch(console.error); - } + // 5) Réponse + await message.reply(reply); + } catch (err) { + console.error("Error processing AI mention:", err); + await message.reply("Oups, mon cerveau a grillé. Réessaie plus tard.").catch(console.error); + } } - // --- Sub-handler for Admin Commands --- async function handleAdminCommands(message) { - const prefix = process.env.DEV_SITE === 'true' ? 'dev' : 'flopo'; - const [command, ...args] = message.content.split(' '); + const prefix = process.env.DEV_SITE === "true" ? "dev" : "flopo"; + const [command, ...args] = message.content.split(" "); - switch(command) { - case '?u': - console.log(await getAPOUsers()); - break; - case '?b': - console.log(await postAPOBuy('650338922874011648', args[0])); - break; - case '?v': - console.log('Active Polls:', activePolls); - break; - case '?sv': - const amount = parseInt(args[0], 10); - if (isNaN(amount)) return message.reply('Invalid amount.'); - let sum = 0; - const start_at = Date.now(); - for (let i = 0; i < amount; i++) { - sum += parseFloat(randomSkinPrice()); - } - console.log(`Result for ${amount} skins: Avg: ~${(sum / amount).toFixed(0)} Flopos | Total: ${sum.toFixed(0)} Flopos | Elapsed: ${Date.now() - start_at}ms`); - break; - case `${prefix}:sotd`: - initTodaysSOTD(); - message.reply('New Solitaire of the Day initialized.'); - break; - case `${prefix}:users`: - console.log(getAllUsers.all()); - break; - case `${prefix}:sql`: - const sqlCommand = args.join(' '); - try { - const stmt = flopoDB.prepare(sqlCommand); - const result = sqlCommand.trim().toUpperCase().startsWith('SELECT') ? stmt.all() : stmt.run(); - console.log(result); - message.reply('```json\n' + JSON.stringify(result, null, 2).substring(0, 1900) + '\n```'); - } catch (e) { - console.error(e); - message.reply(`SQL Error: ${e.message}`); - } - break; - case `${prefix}:fetch-data`: - await getAkhys(client); - break; - case `${prefix}:avatars`: - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const members = await guild.members.fetch(); - const akhys = members.filter(m => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID)); + switch (command) { + case "?u": + console.log(await getAPOUsers()); + break; + case "?b": + console.log(await postAPOBuy("650338922874011648", args[0])); + break; + case "?v": + console.log("Active Polls:", activePolls); + break; + case "?sv": + const amount = parseInt(args[0], 10); + if (isNaN(amount)) return message.reply("Invalid amount."); + let sum = 0; + const start_at = Date.now(); + for (let i = 0; i < amount; i++) { + sum += parseFloat(randomSkinPrice()); + } + console.log( + `Result for ${amount} skins: Avg: ~${(sum / amount).toFixed(0)} Flopos | Total: ${sum.toFixed(0)} Flopos | Elapsed: ${Date.now() - start_at}ms`, + ); + break; + case `${prefix}:sotd`: + initTodaysSOTD(); + message.reply("New Solitaire of the Day initialized."); + break; + case `${prefix}:users`: + console.log(getAllUsers.all()); + break; + case `${prefix}:sql`: + const sqlCommand = args.join(" "); + try { + const stmt = flopoDB.prepare(sqlCommand); + const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") ? stmt.all() : stmt.run(); + console.log(result); + message.reply("```json\n" + JSON.stringify(result, null, 2).substring(0, 1900) + "\n```"); + } catch (e) { + console.error(e); + message.reply(`SQL Error: ${e.message}`); + } + break; + case `${prefix}:fetch-data`: + await getAkhys(client); + break; + case `${prefix}:avatars`: + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const members = await guild.members.fetch(); + const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID)); - const usersToUpdate = akhys.map(akhy => ({ - id: akhy.user.id, - avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }), - })); + const usersToUpdate = akhys.map((akhy) => ({ + id: akhy.user.id, + avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }), + })); - usersToUpdate.forEach(user => { - try { updateUserAvatar.run(user) } catch (err) {} - }) - break; - case `${prefix}:rework-skins`: - console.log("Reworking all skin prices..."); - const dbSkins = getAllSkins.all(); - dbSkins.forEach(skin => { - const fetchedSkin = skins.find(s => s.uuid === skin.uuid); - const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0); - const calculatePrice = () => { - if (!skin.basePrice) return null; - let result = parseFloat(basePrice); - result *= (1 + (skin.currentLvl / Math.max(fetchedSkin.levels.length, 2))); - result *= (1 + (skin.currentChroma / 4)); - return parseFloat(result.toFixed(0)); - }; - const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0); - hardUpdateSkin.run({ - uuid: skin.uuid, - displayName: skin.displayName, - contentTierUuid: skin.contentTierUuid, - displayIcon: skin.displayIcon, - user_id: skin.user_id, - tierRank: skin.tierRank, - tierColor: skin.tierColor, - tierText: skin.tierText, - basePrice: basePrice, - currentLvl: skin.currentLvl || null, - currentChroma: skin.currentChroma || null, - currentPrice: skin.currentPrice ? calculatePrice() : null, - maxPrice: maxPrice, - }) - }) - console.log('Reworked', dbSkins.length, 'skins.'); - break; - case `${prefix}:solve-solitaire`: - autoSolveMoves( - { "tableauPiles": [ [ { "suit": "d", "rank": "K", "faceUp": true }, { "suit": "s", "rank": "Q", "faceUp": true }, { "suit": "d", "rank": "J", "faceUp": true }, { "suit": "c", "rank": "T", "faceUp": true }, { "suit": "h", "rank": "9", "faceUp": true }, { "suit": "c", "rank": "8", "faceUp": true }, { "suit": "h", "rank": "7", "faceUp": true }, { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true } ], [ { "suit": "h", "rank": "K", "faceUp": true }, { "suit": "c", "rank": "Q", "faceUp": true }, { "suit": "h", "rank": "J", "faceUp": true }, { "suit": "s", "rank": "T", "faceUp": true }, { "suit": "d", "rank": "9", "faceUp": true } ], [ { "suit": "s", "rank": "K", "faceUp": true }, { "suit": "d", "rank": "Q", "faceUp": true }, { "suit": "c", "rank": "J", "faceUp": true }, { "suit": "h", "rank": "T", "faceUp": true }, { "suit": "c", "rank": "9", "faceUp": true }, { "suit": "h", "rank": "8", "faceUp": true } ], [], [], [ { "suit": "c", "rank": "K", "faceUp": true }, { "suit": "h", "rank": "Q", "faceUp": true }, { "suit": "s", "rank": "J", "faceUp": true }, { "suit": "d", "rank": "T", "faceUp": true }, { "suit": "s", "rank": "9", "faceUp": true }, { "suit": "d", "rank": "8", "faceUp": true }, { "suit": "c", "rank": "7", "faceUp": true }, { "suit": "h", "rank": "6", "faceUp": true }, { "suit": "c", "rank": "5", "faceUp": true }, { "suit": "h", "rank": "4", "faceUp": true } ], [ { "suit": "h", "rank": "3", "faceUp": true } ] ], "foundationPiles": [ [ { "suit": "c", "rank": "A", "faceUp": true }, { "suit": "c", "rank": "2", "faceUp": true }, { "suit": "c", "rank": "3", "faceUp": true }, { "suit": "c", "rank": "4", "faceUp": true } ], [ { "suit": "h", "rank": "A", "faceUp": true }, { "suit": "h", "rank": "2", "faceUp": true } ], [ { "suit": "s", "rank": "A", "faceUp": true }, { "suit": "s", "rank": "2", "faceUp": true }, { "suit": "s", "rank": "3", "faceUp": true }, { "suit": "s", "rank": "4", "faceUp": true }, { "suit": "s", "rank": "5", "faceUp": true }, { "suit": "s", "rank": "6", "faceUp": true }, { "suit": "s", "rank": "7", "faceUp": true }, { "suit": "s", "rank": "8", "faceUp": true } ], [ { "suit": "d", "rank": "A", "faceUp": true }, { "suit": "d", "rank": "2", "faceUp": true }, { "suit": "d", "rank": "3", "faceUp": true }, { "suit": "d", "rank": "4", "faceUp": true }, { "suit": "d", "rank": "5", "faceUp": true }, { "suit": "d", "rank": "6", "faceUp": true }, { "suit": "d", "rank": "7", "faceUp": true } ] ], "stockPile": [], "wastePile": [], "seed": "mgqnxweyjp8fggj6ol9", "isSOTD": false, "score": 205, "moves": 90, "hist": [ { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 3, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 4, "cardsMoved": [ { "suit": "c", "rank": "9", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 6, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "A", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "c", "rank": "5", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 4, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "4", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "h", "rank": "9", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 4, "destPileType": "foundationPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "A", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "d", "rank": "8", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 6, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "8", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "h", "rank": "2", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "2", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "h", "rank": "Q", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "5", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "h", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "K", "faceUp": true } }, { "move": "draw", "card": { "suit": "c", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "c", "rank": "J", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 4, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "h", "rank": "T", "faceUp": true }, { "suit": "c", "rank": "9", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "4", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "K", "faceUp": true } }, { "move": "draw", "card": { "suit": "s", "rank": "3", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 4, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "s", "rank": "3", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "K", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "7", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "h", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 3, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "h", "rank": "8", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 5, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "h", "rank": "8", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "Q", "faceUp": true } }, { "move": "draw", "card": { "suit": "s", "rank": "A", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 6, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "A", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 6, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "d", "rank": "J", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 0, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "c", "rank": "T", "faceUp": true }, { "suit": "h", "rank": "9", "faceUp": true }, { "suit": "c", "rank": "8", "faceUp": true }, { "suit": "h", "rank": "7", "faceUp": true }, { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "4", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 6, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "c", "rank": "4", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "s", "rank": "5", "faceUp": true } }, { "move": "draw", "card": { "suit": "h", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "d", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "2", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 8, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "2", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "c", "rank": "7", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 8, "destPileType": "tableauPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "c", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 1, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "h", "rank": "6", "faceUp": true }, { "suit": "c", "rank": "5", "faceUp": true }, { "suit": "h", "rank": "4", "faceUp": true }, { "suit": "s", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "5", "faceUp": true } }, { "move": "draw", "card": { "suit": "d", "rank": "A", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 9, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "A", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "c", "rank": "3", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 9, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "3", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "7", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 9, "destPileType": "tableauPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "6", "faceUp": true } }, { "move": "draw-reset" }, { "move": "draw", "card": { "suit": "h", "rank": "Q", "faceUp": true } }, { "move": "draw", "card": { "suit": "d", "rank": "K", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "d", "rank": "K", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 3, "destPileType": "tableauPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "s", "rank": "Q", "faceUp": true }, { "suit": "d", "rank": "J", "faceUp": true }, { "suit": "c", "rank": "T", "faceUp": true }, { "suit": "h", "rank": "9", "faceUp": true }, { "suit": "c", "rank": "8", "faceUp": true }, { "suit": "h", "rank": "7", "faceUp": true }, { "suit": "c", "rank": "6", "faceUp": true }, { "suit": "h", "rank": "5", "faceUp": true }, { "suit": "c", "rank": "4", "faceUp": true }, { "suit": "d", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 2, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "2", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 2, "sourceCardIndex": 8, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 1, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "4", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 1, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "2", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 0, "sourceCardIndex": 10, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "3", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 0, "sourceCardIndex": 9, "destPileType": "foundationPiles", "destPileIndex": 0, "cardsMoved": [ { "suit": "c", "rank": "4", "faceUp": true } ], "cardWasFlipped": true, "points": 11 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 5, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 6, "cardsMoved": [ { "suit": "s", "rank": "7", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "d", "rank": "4", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "4", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "h", "rank": "K", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "K", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "K", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "c", "rank": "K", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "h", "rank": "Q", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 2, "destPileType": "tableauPiles", "destPileIndex": 5, "cardsMoved": [ { "suit": "s", "rank": "J", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "draw", "card": { "suit": "c", "rank": "Q", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "c", "rank": "Q", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "draw", "card": { "suit": "s", "rank": "5", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "h", "rank": "J", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "h", "rank": "J", "faceUp": true } ], "cardWasFlipped": false, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 1, "destPileType": "tableauPiles", "destPileIndex": 1, "cardsMoved": [ { "suit": "s", "rank": "T", "faceUp": true } ], "cardWasFlipped": true, "points": 1 }, { "move": "move", "sourcePileType": "tableauPiles", "sourcePileIndex": 4, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 2, "cardsMoved": [ { "suit": "s", "rank": "6", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "5", "faceUp": true } }, { "move": "move", "sourcePileType": "wastePile", "sourcePileIndex": null, "sourceCardIndex": 0, "destPileType": "foundationPiles", "destPileIndex": 3, "cardsMoved": [ { "suit": "d", "rank": "5", "faceUp": true } ], "cardWasFlipped": false, "points": 11 }, { "move": "draw", "card": { "suit": "d", "rank": "6", "faceUp": true } } ], "hardMode": false, "autocompleting": false } - ); - } -} \ No newline at end of file + usersToUpdate.forEach((user) => { + try { + updateUserAvatar.run(user); + } catch (err) {} + }); + break; + case `${prefix}:rework-skins`: + console.log("Reworking all skin prices..."); + const dbSkins = getAllSkins.all(); + dbSkins.forEach((skin) => { + const fetchedSkin = skins.find((s) => s.uuid === skin.uuid); + const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0); + const calculatePrice = () => { + if (!skin.basePrice) return null; + let result = parseFloat(basePrice); + result *= 1 + skin.currentLvl / Math.max(fetchedSkin.levels.length, 2); + result *= 1 + skin.currentChroma / 4; + return parseFloat(result.toFixed(0)); + }; + const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0); + hardUpdateSkin.run({ + uuid: skin.uuid, + displayName: skin.displayName, + contentTierUuid: skin.contentTierUuid, + displayIcon: skin.displayIcon, + user_id: skin.user_id, + tierRank: skin.tierRank, + tierColor: skin.tierColor, + tierText: skin.tierText, + basePrice: basePrice, + currentLvl: skin.currentLvl || null, + currentChroma: skin.currentChroma || null, + currentPrice: skin.currentPrice ? calculatePrice() : null, + maxPrice: maxPrice, + }); + }); + console.log("Reworked", dbSkins.length, "skins."); + break; + case `${prefix}:solve-solitaire`: + autoSolveMoves({ + tableauPiles: [ + [ + { suit: "d", rank: "K", faceUp: true }, + { suit: "s", rank: "Q", faceUp: true }, + { suit: "d", rank: "J", faceUp: true }, + { suit: "c", rank: "T", faceUp: true }, + { suit: "h", rank: "9", faceUp: true }, + { suit: "c", rank: "8", faceUp: true }, + { suit: "h", rank: "7", faceUp: true }, + { suit: "c", rank: "6", faceUp: true }, + { suit: "h", rank: "5", faceUp: true }, + ], + [ + { suit: "h", rank: "K", faceUp: true }, + { suit: "c", rank: "Q", faceUp: true }, + { suit: "h", rank: "J", faceUp: true }, + { suit: "s", rank: "T", faceUp: true }, + { suit: "d", rank: "9", faceUp: true }, + ], + [ + { suit: "s", rank: "K", faceUp: true }, + { suit: "d", rank: "Q", faceUp: true }, + { suit: "c", rank: "J", faceUp: true }, + { suit: "h", rank: "T", faceUp: true }, + { suit: "c", rank: "9", faceUp: true }, + { suit: "h", rank: "8", faceUp: true }, + ], + [], + [], + [ + { suit: "c", rank: "K", faceUp: true }, + { suit: "h", rank: "Q", faceUp: true }, + { suit: "s", rank: "J", faceUp: true }, + { suit: "d", rank: "T", faceUp: true }, + { suit: "s", rank: "9", faceUp: true }, + { suit: "d", rank: "8", faceUp: true }, + { suit: "c", rank: "7", faceUp: true }, + { suit: "h", rank: "6", faceUp: true }, + { suit: "c", rank: "5", faceUp: true }, + { suit: "h", rank: "4", faceUp: true }, + ], + [{ suit: "h", rank: "3", faceUp: true }], + ], + foundationPiles: [ + [ + { suit: "c", rank: "A", faceUp: true }, + { suit: "c", rank: "2", faceUp: true }, + { suit: "c", rank: "3", faceUp: true }, + { suit: "c", rank: "4", faceUp: true }, + ], + [ + { suit: "h", rank: "A", faceUp: true }, + { suit: "h", rank: "2", faceUp: true }, + ], + [ + { suit: "s", rank: "A", faceUp: true }, + { suit: "s", rank: "2", faceUp: true }, + { suit: "s", rank: "3", faceUp: true }, + { suit: "s", rank: "4", faceUp: true }, + { suit: "s", rank: "5", faceUp: true }, + { suit: "s", rank: "6", faceUp: true }, + { suit: "s", rank: "7", faceUp: true }, + { suit: "s", rank: "8", faceUp: true }, + ], + [ + { suit: "d", rank: "A", faceUp: true }, + { suit: "d", rank: "2", faceUp: true }, + { suit: "d", rank: "3", faceUp: true }, + { suit: "d", rank: "4", faceUp: true }, + { suit: "d", rank: "5", faceUp: true }, + { suit: "d", rank: "6", faceUp: true }, + { suit: "d", rank: "7", faceUp: true }, + ], + ], + stockPile: [], + wastePile: [], + seed: "mgqnxweyjp8fggj6ol9", + isSOTD: false, + score: 205, + moves: 90, + hist: [ + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 3, + sourceCardIndex: 3, + destPileType: "tableauPiles", + destPileIndex: 4, + cardsMoved: [{ suit: "c", rank: "9", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 6, + sourceCardIndex: 6, + destPileType: "foundationPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "c", rank: "A", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 5, + sourceCardIndex: 5, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "c", rank: "5", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 5, + sourceCardIndex: 4, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "h", rank: "4", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 6, + sourceCardIndex: 5, + destPileType: "tableauPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "h", rank: "9", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 6, + sourceCardIndex: 4, + destPileType: "foundationPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "h", rank: "A", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 6, + sourceCardIndex: 3, + destPileType: "tableauPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "d", rank: "8", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 6, + sourceCardIndex: 2, + destPileType: "tableauPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "c", rank: "8", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { move: "draw", card: { suit: "h", rank: "2", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 0, + destPileType: "foundationPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "h", rank: "2", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "h", rank: "Q", faceUp: true } }, + { move: "draw", card: { suit: "h", rank: "5", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 1, + destPileType: "tableauPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "h", rank: "5", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "d", rank: "K", faceUp: true } }, + { move: "draw", card: { suit: "c", rank: "J", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 2, + destPileType: "tableauPiles", + destPileIndex: 6, + cardsMoved: [{ suit: "c", rank: "J", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 4, + sourceCardIndex: 4, + destPileType: "tableauPiles", + destPileIndex: 6, + cardsMoved: [ + { suit: "h", rank: "T", faceUp: true }, + { suit: "c", rank: "9", faceUp: true }, + ], + cardWasFlipped: true, + points: 1, + }, + { move: "draw", card: { suit: "d", rank: "4", faceUp: true } }, + { move: "draw", card: { suit: "h", rank: "K", faceUp: true } }, + { move: "draw", card: { suit: "s", rank: "3", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 4, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "s", rank: "3", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "c", rank: "K", faceUp: true } }, + { move: "draw", card: { suit: "h", rank: "7", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 5, + destPileType: "tableauPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "h", rank: "7", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 3, + sourceCardIndex: 2, + destPileType: "tableauPiles", + destPileIndex: 0, + cardsMoved: [ + { suit: "c", rank: "6", faceUp: true }, + { suit: "h", rank: "5", faceUp: true }, + ], + cardWasFlipped: true, + points: 1, + }, + { move: "draw", card: { suit: "h", rank: "8", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 5, + destPileType: "tableauPiles", + destPileIndex: 6, + cardsMoved: [{ suit: "h", rank: "8", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "c", rank: "Q", faceUp: true } }, + { move: "draw", card: { suit: "s", rank: "A", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 6, + destPileType: "foundationPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "s", rank: "A", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "d", rank: "J", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 6, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [{ suit: "d", rank: "J", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 0, + sourceCardIndex: 0, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [ + { suit: "c", rank: "T", faceUp: true }, + { suit: "h", rank: "9", faceUp: true }, + { suit: "c", rank: "8", faceUp: true }, + { suit: "h", rank: "7", faceUp: true }, + { suit: "c", rank: "6", faceUp: true }, + { suit: "h", rank: "5", faceUp: true }, + ], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "c", rank: "4", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 6, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [{ suit: "c", rank: "4", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "s", rank: "5", faceUp: true } }, + { move: "draw", card: { suit: "h", rank: "J", faceUp: true } }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 4, + sourceCardIndex: 3, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [{ suit: "d", rank: "3", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { move: "draw", card: { suit: "c", rank: "2", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 8, + destPileType: "foundationPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "c", rank: "2", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "c", rank: "7", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 8, + destPileType: "tableauPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "c", rank: "7", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 1, + sourceCardIndex: 1, + destPileType: "tableauPiles", + destPileIndex: 2, + cardsMoved: [ + { suit: "h", rank: "6", faceUp: true }, + { suit: "c", rank: "5", faceUp: true }, + { suit: "h", rank: "4", faceUp: true }, + { suit: "s", rank: "3", faceUp: true }, + ], + cardWasFlipped: true, + points: 1, + }, + { move: "draw", card: { suit: "d", rank: "5", faceUp: true } }, + { move: "draw", card: { suit: "d", rank: "A", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 9, + destPileType: "foundationPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "d", rank: "A", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "c", rank: "3", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 9, + destPileType: "foundationPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "c", rank: "3", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "d", rank: "7", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 9, + destPileType: "tableauPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "d", rank: "7", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "d", rank: "6", faceUp: true } }, + { move: "draw-reset" }, + { move: "draw", card: { suit: "h", rank: "Q", faceUp: true } }, + { move: "draw", card: { suit: "d", rank: "K", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 1, + destPileType: "tableauPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "d", rank: "K", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 5, + sourceCardIndex: 3, + destPileType: "tableauPiles", + destPileIndex: 0, + cardsMoved: [ + { suit: "s", rank: "Q", faceUp: true }, + { suit: "d", rank: "J", faceUp: true }, + { suit: "c", rank: "T", faceUp: true }, + { suit: "h", rank: "9", faceUp: true }, + { suit: "c", rank: "8", faceUp: true }, + { suit: "h", rank: "7", faceUp: true }, + { suit: "c", rank: "6", faceUp: true }, + { suit: "h", rank: "5", faceUp: true }, + { suit: "c", rank: "4", faceUp: true }, + { suit: "d", rank: "3", faceUp: true }, + ], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 5, + sourceCardIndex: 2, + destPileType: "foundationPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "s", rank: "2", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 2, + sourceCardIndex: 8, + destPileType: "foundationPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "s", rank: "3", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 1, + sourceCardIndex: 0, + destPileType: "foundationPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "s", rank: "4", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 5, + sourceCardIndex: 1, + destPileType: "foundationPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "d", rank: "2", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 0, + sourceCardIndex: 10, + destPileType: "foundationPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "d", rank: "3", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 0, + sourceCardIndex: 9, + destPileType: "foundationPiles", + destPileIndex: 0, + cardsMoved: [{ suit: "c", rank: "4", faceUp: true }], + cardWasFlipped: true, + points: 11, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 5, + sourceCardIndex: 0, + destPileType: "tableauPiles", + destPileIndex: 6, + cardsMoved: [{ suit: "s", rank: "7", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "d", rank: "4", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 1, + destPileType: "foundationPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "d", rank: "4", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "h", rank: "K", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 1, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "h", rank: "K", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "c", rank: "K", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 1, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [{ suit: "c", rank: "K", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 0, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [{ suit: "h", rank: "Q", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 4, + sourceCardIndex: 2, + destPileType: "tableauPiles", + destPileIndex: 5, + cardsMoved: [{ suit: "s", rank: "J", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { move: "draw", card: { suit: "c", rank: "Q", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 0, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "c", rank: "Q", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { move: "draw", card: { suit: "s", rank: "5", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 0, + destPileType: "foundationPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "s", rank: "5", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "h", rank: "J", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 0, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "h", rank: "J", faceUp: true }], + cardWasFlipped: false, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 4, + sourceCardIndex: 1, + destPileType: "tableauPiles", + destPileIndex: 1, + cardsMoved: [{ suit: "s", rank: "T", faceUp: true }], + cardWasFlipped: true, + points: 1, + }, + { + move: "move", + sourcePileType: "tableauPiles", + sourcePileIndex: 4, + sourceCardIndex: 0, + destPileType: "foundationPiles", + destPileIndex: 2, + cardsMoved: [{ suit: "s", rank: "6", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "d", rank: "5", faceUp: true } }, + { + move: "move", + sourcePileType: "wastePile", + sourcePileIndex: null, + sourceCardIndex: 0, + destPileType: "foundationPiles", + destPileIndex: 3, + cardsMoved: [{ suit: "d", rank: "5", faceUp: true }], + cardWasFlipped: false, + points: 11, + }, + { move: "draw", card: { suit: "d", rank: "6", faceUp: true } }, + ], + hardMode: false, + autocompleting: false, + }); + } +} diff --git a/src/config/commands.js b/src/config/commands.js index d3210ce..309e2aa 100644 --- a/src/config/commands.js +++ b/src/config/commands.js @@ -1,113 +1,121 @@ -import 'dotenv/config'; -import { getTimesChoices } from '../game/various.js'; -import { capitalize, InstallGlobalCommands } from '../utils/index.js'; +import "dotenv/config"; +import { getTimesChoices } from "../game/various.js"; +import { capitalize, InstallGlobalCommands } from "../utils/index.js"; function createTimesChoices() { - const choices = getTimesChoices(); - const commandChoices = []; + const choices = getTimesChoices(); + const commandChoices = []; - for (let choice of choices) { - commandChoices.push({ - name: capitalize(choice.name), - value: choice.value?.toString(), - }); - } + for (let choice of choices) { + commandChoices.push({ + name: capitalize(choice.name), + value: choice.value?.toString(), + }); + } - return commandChoices; + return commandChoices; } // Timeout vote command const TIMEOUT_COMMAND = { - name: 'timeout', - description: 'Vote démocratique pour timeout un boug', - options: [ - { - type: 6, - name: 'akhy', - description: 'Qui ?', - required: true, - }, - { - type: 3, - name: 'temps', - description: 'Combien de temps ?', - required: true, - choices: createTimesChoices(), - } - ], - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "timeout", + description: "Vote démocratique pour timeout un boug", + options: [ + { + type: 6, + name: "akhy", + description: "Qui ?", + required: true, + }, + { + type: 3, + name: "temps", + description: "Combien de temps ?", + required: true, + choices: createTimesChoices(), + }, + ], + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; // Valorant const VALORANT_COMMAND = { - name: 'valorant', - description: `Ouvrir une caisse valorant (500 FlopoCoins)`, - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "valorant", + description: `Ouvrir une caisse valorant (500 FlopoCoins)`, + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; // Own inventory command const INVENTORY_COMMAND = { - name: 'inventory', - description: 'Voir inventaire', - options: [ - { - type: 6, - name: 'akhy', - description: 'Qui ?', - required: false, - }, - ], - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "inventory", + description: "Voir inventaire", + options: [ + { + type: 6, + name: "akhy", + description: "Qui ?", + required: false, + }, + ], + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; const INFO_COMMAND = { - name: 'info', - description: 'Qui est time out ?', - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "info", + description: "Qui est time out ?", + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; const SKINS_COMMAND = { - name: 'skins', - description: 'Le top 10 des skins les plus chers.', - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "skins", + description: "Le top 10 des skins les plus chers.", + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; const SITE_COMMAND = { - name: 'floposite', - description: 'Lien vers FlopoSite', - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "floposite", + description: "Lien vers FlopoSite", + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; const SEARCH_SKIN_COMMAND = { - name: 'search', - description: 'Chercher un skin', - options: [ - { - type: 3, - name: 'recherche', - description: 'Tu cherches quoi ?', - required: true, - }, - ], - type: 1, - integration_types: [0, 1], - contexts: [0, 2], -} + name: "search", + description: "Chercher un skin", + options: [ + { + type: 3, + name: "recherche", + description: "Tu cherches quoi ?", + required: true, + }, + ], + type: 1, + integration_types: [0, 1], + contexts: [0, 2], +}; -const ALL_COMMANDS = [TIMEOUT_COMMAND, INVENTORY_COMMAND, VALORANT_COMMAND, INFO_COMMAND, SKINS_COMMAND, SEARCH_SKIN_COMMAND, SITE_COMMAND]; +const ALL_COMMANDS = [ + TIMEOUT_COMMAND, + INVENTORY_COMMAND, + VALORANT_COMMAND, + INFO_COMMAND, + SKINS_COMMAND, + SEARCH_SKIN_COMMAND, + SITE_COMMAND, +]; export function registerCommands() { - InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS); -} \ No newline at end of file + InstallGlobalCommands(process.env.APP_ID, ALL_COMMANDS); +} diff --git a/src/database/index.js b/src/database/index.js index 93038ab..04c5e3b 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -1,190 +1,381 @@ import Database from "better-sqlite3"; - -export const flopoDB = new Database('flopobot.db'); +export const flopoDB = new Database("flopobot.db"); export const stmtUsers = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - globalName TEXT, - warned BOOLEAN DEFAULT 0, - warns INTEGER DEFAULT 0, - allTimeWarns INTEGER DEFAULT 0, - totalRequests INTEGER DEFAULT 0, - coins INTEGER DEFAULT 0, - dailyQueried BOOLEAN DEFAULT 0, - avatarUrl TEXT DEFAULT NULL, - isAkhy BOOLEAN DEFAULT 0 - ) + CREATE TABLE IF NOT EXISTS users + ( + id TEXT PRIMARY KEY, + username TEXT NOT NULL, + globalName TEXT, + warned BOOLEAN DEFAULT 0, + warns INTEGER DEFAULT 0, + allTimeWarns INTEGER DEFAULT 0, + totalRequests INTEGER DEFAULT 0, + coins INTEGER DEFAULT 0, + dailyQueried BOOLEAN DEFAULT 0, + avatarUrl TEXT DEFAULT NULL, + isAkhy BOOLEAN DEFAULT 0 + ) `); stmtUsers.run(); export const stmtSkins = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS skins ( - uuid TEXT PRIMARY KEY, - displayName TEXT, - contentTierUuid TEXT, - displayIcon TEXT, - user_id TEXT REFERENCES users, - tierRank TEXT, - tierColor TEXT, - tierText TEXT, - basePrice TEXT, - currentLvl INTEGER DEFAULT NULL, - currentChroma INTEGER DEFAULT NULL, - currentPrice INTEGER DEFAULT NULL, - maxPrice INTEGER DEFAULT NULL - ) + CREATE TABLE IF NOT EXISTS skins + ( + uuid TEXT PRIMARY KEY, + displayName TEXT, + contentTierUuid TEXT, + displayIcon TEXT, + user_id TEXT REFERENCES users, + tierRank TEXT, + tierColor TEXT, + tierText TEXT, + basePrice TEXT, + currentLvl INTEGER DEFAULT NULL, + currentChroma INTEGER DEFAULT NULL, + currentPrice INTEGER DEFAULT NULL, + maxPrice INTEGER DEFAULT NULL + ) `); -stmtSkins.run() +stmtSkins.run(); -export const insertUser = flopoDB.prepare('INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy) VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)'); -export const updateUser = flopoDB.prepare('UPDATE users SET warned = @warned, warns = @warns, allTimeWarns = @allTimeWarns, totalRequests = @totalRequests WHERE id = @id'); -export const updateUserAvatar = flopoDB.prepare('UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id'); -export const queryDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 1 WHERE id = ?`); -export const resetDailyReward = flopoDB.prepare(`UPDATE users SET dailyQueried = 0`); -export const updateUserCoins = flopoDB.prepare('UPDATE users SET coins = @coins WHERE id = @id'); -export const getUser = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?'); -export const getAllUsers = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC'); -export const getAllAkhys = flopoDB.prepare('SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC'); +export const insertUser = flopoDB.prepare( + `INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy) + VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`, +); +export const updateUser = flopoDB.prepare( + `UPDATE users + SET warned = @warned, + warns = @warns, + allTimeWarns = @allTimeWarns, + totalRequests = @totalRequests + WHERE id = @id`, +); +export const updateUserAvatar = flopoDB.prepare("UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id"); +export const queryDailyReward = flopoDB.prepare(`UPDATE users + SET dailyQueried = 1 + WHERE id = ?`); +export const resetDailyReward = flopoDB.prepare(`UPDATE users + SET dailyQueried = 0`); +export const updateUserCoins = flopoDB.prepare("UPDATE users SET coins = @coins WHERE id = @id"); +export const getUser = flopoDB.prepare( + "SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?", +); +export const getAllUsers = flopoDB.prepare( + "SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC", +); +export const getAllAkhys = flopoDB.prepare( + "SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC", +); -export const insertSkin = flopoDB.prepare('INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, basePrice, currentLvl, currentChroma, currentPrice, maxPrice) VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)'); -export const updateSkin = flopoDB.prepare('UPDATE skins SET user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice WHERE uuid = @uuid'); -export const hardUpdateSkin = flopoDB.prepare('UPDATE skins SET displayName = @displayName, contentTierUuid = @contentTierUuid, displayIcon = @displayIcon, tierRank = @tierRank, tierColor = @tierColor, tierText = @tierText, basePrice = @basePrice, user_id = @user_id, currentLvl = @currentLvl, currentChroma = @currentChroma, currentPrice = @currentPrice, maxPrice = @maxPrice WHERE uuid = @uuid'); -export const getSkin = flopoDB.prepare('SELECT * FROM skins WHERE uuid = ?'); -export const getAllSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC'); -export const getAllAvailableSkins = flopoDB.prepare('SELECT * FROM skins WHERE user_id IS NULL'); -export const getUserInventory = flopoDB.prepare('SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC'); -export const getTopSkins = flopoDB.prepare('SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10'); +export const insertSkin = flopoDB.prepare( + `INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, + basePrice, currentLvl, currentChroma, currentPrice, maxPrice) + VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, + @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)`, +); +export const updateSkin = flopoDB.prepare( + `UPDATE skins + SET user_id = @user_id, + currentLvl = @currentLvl, + currentChroma = @currentChroma, + currentPrice = @currentPrice + WHERE uuid = @uuid`, +); +export const hardUpdateSkin = flopoDB.prepare( + `UPDATE skins + SET displayName = @displayName, + contentTierUuid = @contentTierUuid, + displayIcon = @displayIcon, + tierRank = @tierRank, + tierColor = @tierColor, + tierText = @tierText, + basePrice = @basePrice, + user_id = @user_id, + currentLvl = @currentLvl, + currentChroma = @currentChroma, + currentPrice = @currentPrice, + maxPrice = @maxPrice + WHERE uuid = @uuid`, +); +export const getSkin = flopoDB.prepare("SELECT * FROM skins WHERE uuid = ?"); +export const getAllSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC"); +export const getAllAvailableSkins = flopoDB.prepare("SELECT * FROM skins WHERE user_id IS NULL"); +export const getUserInventory = flopoDB.prepare( + "SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC", +); +export const getTopSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10"); + +export const stmtMarketOffers = flopoDB.prepare(` + CREATE TABLE IF NOT EXISTS market_offers + ( + id PRIMARY KEY, + skin_uuid TEXT REFERENCES skins, + seller_id TEXT REFERENCES users, + starting_price INTEGER NOT NULL, + buyout_price INTEGER DEFAULT NULL, + final_price INTEGER DEFAULT NULL, + status TEXT NOT NULL, + posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + opening_at TIMESTAMP NOT NULL, + closing_at TIMESTAMP NOT NULL, + buyer_id TEXT REFERENCES users DEFAULT NULL + ) +`); +stmtMarketOffers.run(); + +export const stmtBids = flopoDB.prepare(` + CREATE TABLE IF NOT EXISTS bids + ( + id PRIMARY KEY, + bidder_id TEXT REFERENCES users, + market_offer_id REFERENCES market_offers, + offer_amount INTEGER, + offered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) +`); +stmtBids.run(); + +export const getMarketOffers = flopoDB.prepare(` + SELECT market_offers.*, + skins.displayName AS skinName, + skins.displayIcon AS skinIcon, + seller.username AS sellerName, + seller.globalName AS sellerGlobalName, + buyer.username AS buyerName, + buyer.globalName AS buyerGlobalName + FROM market_offers + JOIN skins ON skins.uuid = market_offers.skin_uuid + JOIN users AS seller ON seller.id = market_offers.seller_id + LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id + ORDER BY market_offers.posted_at DESC +`); + +export const getMarketOfferById = flopoDB.prepare(` + SELECT market_offers.*, + skins.displayName AS skinName, + skins.displayIcon AS skinIcon, + seller.username AS sellerName, + seller.globalName AS sellerGlobalName, + buyer.username AS buyerName, + buyer.globalName AS buyerGlobalName + FROM market_offers + JOIN skins ON skins.uuid = market_offers.skin_uuid + JOIN users AS seller ON seller.id = market_offers.seller_id + LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id + WHERE market_offers.id = ? +`); + +export const insertMarketOffer = flopoDB.prepare(` + INSERT INTO market_offers (id, skin_uuid, seller_id, starting_price, buyout_price, status, opening_at, closing_at) + VALUES (@id, @skin_uuid, @seller_id, @starting_price, @buyout_price, @status, @opening_at, @closing_at) +`); + +export const getBids = flopoDB.prepare(` + SELECT bids.*, + bidder.username AS bidderName, + bidder.globalName AS bidderGlobalName + FROM bids + JOIN users AS bidder ON bidder.id = bids.bidder_id + ORDER BY bids.offer_amount DESC, bids.offered_at ASC +`); + +export const getBidById = flopoDB.prepare(` + SELECT bids.* + FROM bids + WHERE bids.id = ? +`); + +export const getOfferBids = flopoDB.prepare(` + SELECT bids.* + FROM bids + WHERE bids.market_offer_id = ? + ORDER BY bids.offer_amount DESC, bids.offered_at ASC +`); + +export const insertBid = flopoDB.prepare(` + INSERT INTO bids (id, bidder_id, market_offer_id, offer_amount) + VALUES (@id, @bidder_id, @market_offer_id, @offer_amount) +`); export const insertManyUsers = flopoDB.transaction(async (users) => { - for (const user of users) try { await insertUser.run(user) } catch (e) { /**/ } + for (const user of users) + try { + await insertUser.run(user); + } catch (e) {} }); export const updateManyUsers = flopoDB.transaction(async (users) => { - for (const user of users) try { await updateUser.run(user) } catch (e) { console.log('user update failed') } + for (const user of users) + try { + await updateUser.run(user); + } catch (e) { + console.log("user update failed"); + } }); export const insertManySkins = flopoDB.transaction(async (skins) => { - for (const skin of skins) try { await insertSkin.run(skin) } catch (e) {} + for (const skin of skins) + try { + await insertSkin.run(skin); + } catch (e) {} }); export const updateManySkins = flopoDB.transaction(async (skins) => { - for (const skin of skins) try { await updateSkin.run(skin) } catch (e) {} + for (const skin of skins) + try { + await updateSkin.run(skin); + } catch (e) {} }); - export const stmtLogs = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS logs ( - id PRIMARY KEY, - user_id TEXT REFERENCES users, - action TEXT, - target_user_id TEXT REFERENCES users, - coins_amount INTEGER, - user_new_amount INTEGER - ) + CREATE TABLE IF NOT EXISTS logs + ( + id PRIMARY KEY, + user_id TEXT REFERENCES users, + action TEXT, + target_user_id TEXT REFERENCES users, + coins_amount INTEGER, + user_new_amount INTEGER + ) `); -stmtLogs.run() - -export const insertLog = flopoDB.prepare('INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount) VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)'); -export const getLogs = flopoDB.prepare('SELECT * FROM logs'); -export const getUserLogs = flopoDB.prepare('SELECT * FROM logs WHERE user_id = @user_id'); +stmtLogs.run(); +export const insertLog = flopoDB.prepare( + `INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount) + VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)`, +); +export const getLogs = flopoDB.prepare("SELECT * FROM logs"); +export const getUserLogs = flopoDB.prepare("SELECT * FROM logs WHERE user_id = @user_id"); export const stmtGames = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS games ( - id PRIMARY KEY, - p1 TEXT REFERENCES users, - p2 TEXT REFERENCES users, - p1_score INTEGER, - p2_score INTEGER, - p1_elo INTEGER, - p2_elo INTEGER, - p1_new_elo INTEGER, - p2_new_elo INTEGER, - type TEXT, - timestamp TIMESTAMP - ) + CREATE TABLE IF NOT EXISTS games + ( + id PRIMARY KEY, + p1 TEXT REFERENCES users, + p2 TEXT REFERENCES users, + p1_score INTEGER, + p2_score INTEGER, + p1_elo INTEGER, + p2_elo INTEGER, + p1_new_elo INTEGER, + p2_new_elo INTEGER, + type TEXT, + timestamp TIMESTAMP + ) `); -stmtGames.run() - -export const insertGame = flopoDB.prepare('INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp) VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)'); -export const getGames = flopoDB.prepare('SELECT * FROM games'); -export const getUserGames = flopoDB.prepare('SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp'); +stmtGames.run(); +export const insertGame = flopoDB.prepare( + `INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp) + VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)`, +); +export const getGames = flopoDB.prepare("SELECT * FROM games"); +export const getUserGames = flopoDB.prepare( + "SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp", +); export const stmtElos = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS elos ( - id PRIMARY KEY REFERENCES users, - elo INTEGER - ) + CREATE TABLE IF NOT EXISTS elos + ( + id PRIMARY KEY REFERENCES users, + elo INTEGER + ) `); -stmtElos.run() +stmtElos.run(); -export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo) VALUES (@id, @elo)`); -export const getElos = flopoDB.prepare(`SELECT * FROM elos`); -export const getUserElo = flopoDB.prepare(`SELECT * FROM elos WHERE id = @id`); -export const updateElo = flopoDB.prepare('UPDATE elos SET elo = @elo WHERE id = @id'); +export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo) + VALUES (@id, @elo)`); +export const getElos = flopoDB.prepare(`SELECT * + FROM elos`); +export const getUserElo = flopoDB.prepare(`SELECT * + FROM elos + WHERE id = @id`); +export const updateElo = flopoDB.prepare("UPDATE elos SET elo = @elo WHERE id = @id"); - -export const getUsersByElo = flopoDB.prepare('SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC') +export const getUsersByElo = flopoDB.prepare( + "SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC", +); export const stmtSOTD = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS sotd ( - id INT PRIMARY KEY, - tableauPiles TEXT, - foundationPiles TEXT, - stockPile TEXT, - wastePile TEXT, - isDone BOOLEAN DEFAULT false, - seed TEXT - ) + CREATE TABLE IF NOT EXISTS sotd + ( + id INT PRIMARY KEY, + tableauPiles TEXT, + foundationPiles TEXT, + stockPile TEXT, + wastePile TEXT, + isDone BOOLEAN DEFAULT false, + seed TEXT + ) `); -stmtSOTD.run() +stmtSOTD.run(); -export const getSOTD = flopoDB.prepare(`SELECT * FROM sotd WHERE id = '0'`) -export const insertSOTD = flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`) -export const deleteSOTD = flopoDB.prepare(`DELETE FROM sotd WHERE id = '0'`) +export const getSOTD = flopoDB.prepare(`SELECT * + FROM sotd + WHERE id = '0'`); +export const insertSOTD = + flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed) + VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`); +export const deleteSOTD = flopoDB.prepare(`DELETE + FROM sotd + WHERE id = '0'`); export const stmtSOTDStats = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS sotd_stats ( - id TEXT PRIMARY KEY, - user_id TEXT REFERENCES users, - time INTEGER, - moves INTEGER, - score INTEGER - ) + CREATE TABLE IF NOT EXISTS sotd_stats + ( + id TEXT PRIMARY KEY, + user_id TEXT REFERENCES users, + time INTEGER, + moves INTEGER, + score INTEGER + ) `); -stmtSOTDStats.run() +stmtSOTDStats.run(); -export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName FROM sotd_stats JOIN users ON users.id = sotd_stats.user_id ORDER BY score DESC, moves ASC, time ASC`); -export const getUserSOTDStats = flopoDB.prepare(`SELECT * FROM sotd_stats WHERE user_id = ?`); -export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) VALUES (@id, @user_id, @time, @moves, @score)`); -export const clearSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats`); -export const deleteUserSOTDStats = flopoDB.prepare(`DELETE FROM sotd_stats WHERE user_id = ?`); +export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName + FROM sotd_stats + JOIN users ON users.id = sotd_stats.user_id + ORDER BY score DESC, moves ASC, time ASC`); +export const getUserSOTDStats = flopoDB.prepare(`SELECT * + FROM sotd_stats + WHERE user_id = ?`); +export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score) + VALUES (@id, @user_id, @time, @moves, @score)`); +export const clearSOTDStats = flopoDB.prepare(`DELETE + FROM sotd_stats`); +export const deleteUserSOTDStats = flopoDB.prepare(`DELETE + FROM sotd_stats + WHERE user_id = ?`); export async function pruneOldLogs() { - const users = flopoDB.prepare(` - SELECT user_id - FROM logs - GROUP BY user_id - HAVING COUNT(*) > ${process.env.LOGS_BY_USER} - `).all(); + const users = flopoDB + .prepare( + ` + SELECT user_id + FROM logs + GROUP BY user_id + HAVING COUNT(*) > ${process.env.LOGS_BY_USER} + `, + ) + .all(); - const transaction = flopoDB.transaction(() => { - for (const { user_id } of users) { - flopoDB.prepare(` - DELETE FROM logs - WHERE id IN ( - SELECT id FROM ( - SELECT id, - ROW_NUMBER() OVER (ORDER BY id DESC) AS rn - FROM logs - WHERE user_id = ? - ) - WHERE rn > ${process.env.LOGS_BY_USER} - ) - `).run(user_id); - } - }); + const transaction = flopoDB.transaction(() => { + for (const { user_id } of users) { + flopoDB + .prepare( + ` + DELETE + FROM logs + WHERE id IN (SELECT id + FROM (SELECT id, + ROW_NUMBER() OVER (ORDER BY id DESC) AS rn + FROM logs + WHERE user_id = ?) + WHERE rn > ${process.env.LOGS_BY_USER}) + `, + ) + .run(user_id); + } + }); - transaction() -} \ No newline at end of file + transaction(); +} diff --git a/src/game/blackjack.js b/src/game/blackjack.js index f384114..26da141 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -2,384 +2,425 @@ // Core blackjack helpers for a single continuous room. // Inspired by your poker helpers API style. -import {emitToast} from "../server/socket.js"; -import {getUser, insertLog, updateUserCoins} from "../database/index.js"; -import {client} from "../bot/client.js"; -import {EmbedBuilder} from "discord.js"; +import { emitToast } from "../server/socket.js"; +import { getUser, insertLog, updateUserCoins } from "../database/index.js"; +import { client } from "../bot/client.js"; +import { EmbedBuilder } from "discord.js"; -export const RANKS = ["A","2","3","4","5","6","7","8","9","T","J","Q","K"]; -export const SUITS = ["d","s","c","h"]; +export const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"]; +export const SUITS = ["d", "s", "c", "h"]; // Build a single 52-card deck like "Ad","Ts", etc. -export const singleDeck = RANKS.flatMap(r => SUITS.map(s => `${r}${s}`)); +export const singleDeck = RANKS.flatMap((r) => SUITS.map((s) => `${r}${s}`)); export function buildShoe(decks = 6) { - const shoe = []; - for (let i = 0; i < decks; i++) shoe.push(...singleDeck); - return shuffle(shoe); + const shoe = []; + for (let i = 0; i < decks; i++) shoe.push(...singleDeck); + return shuffle(shoe); } export function shuffle(arr) { - // Fisher–Yates - const a = [...arr]; - for (let i = a.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [a[i], a[j]] = [a[j], a[i]]; - } - return a; + // Fisher–Yates + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; } // Draw one card from the shoe; if empty, caller should reshuffle at end of round. export function draw(shoe) { - return shoe.pop(); + return shoe.pop(); } // Return an object describing the best value of a hand with flexible Aces. export function handValue(cards) { - // Count with all aces as 11, then reduce as needed - let total = 0; - let aces = 0; - for (const c of cards) { - const r = c[0]; - if (r === "A") { total += 11; aces += 1; } - else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10; - else total += Number(r); - } - while (total > 21 && aces > 0) { - total -= 10; // convert an Ace from 11 to 1 - aces -= 1; - } - const soft = (aces > 0); // if any Ace still counted as 11, it's a soft hand - return { total, soft }; + // Count with all aces as 11, then reduce as needed + let total = 0; + let aces = 0; + for (const c of cards) { + const r = c[0]; + if (r === "A") { + total += 11; + aces += 1; + } else if (r === "T" || r === "J" || r === "Q" || r === "K") total += 10; + else total += Number(r); + } + while (total > 21 && aces > 0) { + total -= 10; // convert an Ace from 11 to 1 + aces -= 1; + } + const soft = aces > 0; // if any Ace still counted as 11, it's a soft hand + return { total, soft }; } export function isBlackjack(cards) { - return cards.length === 2 && handValue(cards).total === 21; + return cards.length === 2 && handValue(cards).total === 21; } export function isBust(cards) { - return handValue(cards).total > 21; + return handValue(cards).total > 21; } // Dealer draw rule. By default, dealer stands on soft 17 (S17). export function dealerShouldHit(dealerCards, hitSoft17 = false) { - const v = handValue(dealerCards); - if (v.total < 17) return true; - if (v.total === 17 && v.soft && hitSoft17) return true; - return false; + const v = handValue(dealerCards); + if (v.total < 17) return true; + if (v.total === 17 && v.soft && hitSoft17) return true; + return false; } // Compare a player hand to dealer and return outcome. export function compareHands(playerCards, dealerCards) { - const pv = handValue(playerCards).total; - const dv = handValue(dealerCards).total; - if (pv > 21) return "lose"; - if (dv > 21) return "win"; - if (pv > dv) return "win"; - if (pv < dv) return "lose"; - return "push"; + const pv = handValue(playerCards).total; + const dv = handValue(dealerCards).total; + if (pv > 21) return "lose"; + if (dv > 21) return "win"; + if (pv > dv) return "win"; + if (pv < dv) return "lose"; + return "push"; } // Compute payout for a single finished hand (no splits here). // options: { blackjackPayout: 1.5, allowSurrender: false } -export function settleHand({ bet, playerCards, dealerCards, doubled = false, surrendered = false, blackjackPayout = 1.5 }) { - if (surrendered) return { delta: -bet / 2, result: "surrender" }; +export function settleHand({ + bet, + playerCards, + dealerCards, + doubled = false, + surrendered = false, + blackjackPayout = 1.5, +}) { + if (surrendered) return { delta: -bet / 2, result: "surrender" }; - const pBJ = isBlackjack(playerCards); - const dBJ = isBlackjack(dealerCards); + const pBJ = isBlackjack(playerCards); + const dBJ = isBlackjack(dealerCards); - if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" }; - if (!pBJ && dBJ) return { delta: -bet, result: "lose" }; - if (pBJ && dBJ) return { delta: 0, result: "push" }; + if (pBJ && !dBJ) return { delta: bet * blackjackPayout, result: "blackjack" }; + if (!pBJ && dBJ) return { delta: -bet, result: "lose" }; + if (pBJ && dBJ) return { delta: 0, result: "push" }; - const outcome = compareHands(playerCards, dealerCards); - let unit = bet; - if (outcome === "win") return { delta: unit, result: "win" }; - if (outcome === "lose") return { delta: -unit, result: "lose" }; - return { delta: 0, result: "push" }; + const outcome = compareHands(playerCards, dealerCards); + let unit = bet; + if (outcome === "win") return { delta: unit, result: "win" }; + if (outcome === "lose") return { delta: -unit, result: "lose" }; + return { delta: 0, result: "push" }; } // Helper to decide if doubling is still allowed (first decision, 2 cards, not hit yet). export function canDouble(hand) { - return hand.cards.length === 2 && !hand.hasActed; + return hand.cards.length === 2 && !hand.hasActed; } // Very small utility to format a public-safe snapshot of room state export function publicPlayerView(player) { - // Hide hole cards until dealer reveal is fine for dealer only; player cards are visible. - return { - id: player.id, - globalName: player.globalName, - avatar: player.avatar, - bank: player.bank, - currentBet: player.currentBet, - inRound: player.inRound, - hands: player.hands.map(h => ({ - cards: h.cards, - stood: h.stood, - busted: h.busted, - doubled: h.doubled, - surrendered: h.surrendered, - result: h.result ?? null, - total: handValue(h.cards).total, - soft: handValue(h.cards).soft, - bet: h.bet, - })), - }; + // Hide hole cards until dealer reveal is fine for dealer only; player cards are visible. + return { + id: player.id, + globalName: player.globalName, + avatar: player.avatar, + bank: player.bank, + currentBet: player.currentBet, + inRound: player.inRound, + hands: player.hands.map((h) => ({ + cards: h.cards, + stood: h.stood, + busted: h.busted, + doubled: h.doubled, + surrendered: h.surrendered, + result: h.result ?? null, + total: handValue(h.cards).total, + soft: handValue(h.cards).soft, + bet: h.bet, + })), + }; } // Build initial room object export function createBlackjackRoom({ - minBet = 10, - maxBet = 10000, - fakeMoney = false, - decks = 6, - hitSoft17 = false, - blackjackPayout = 1.5, - cutCardRatio = 0.25, // reshuffle when 25% of shoe remains - phaseDurations = { - bettingMs: 15000, - dealMs: 1000, - playMsPerPlayer: 20000, - revealMs: 1000, - payoutMs: 10000, - }, - animation = { - dealerDrawMs: 500, - } + minBet = 10, + maxBet = 10000, + fakeMoney = false, + decks = 6, + hitSoft17 = false, + blackjackPayout = 1.5, + cutCardRatio = 0.25, // reshuffle when 25% of shoe remains + phaseDurations = { + bettingMs: 15000, + dealMs: 1000, + playMsPerPlayer: 20000, + revealMs: 1000, + payoutMs: 10000, + }, + animation = { + dealerDrawMs: 500, + }, } = {}) { - return { - id: "blackjack-room", - name: "Blackjack", - created_at: Date.now(), - status: "betting", // betting | dealing | playing | dealer | payout | shuffle - phase_ends_at: Date.now() + phaseDurations.bettingMs, - minBet, maxBet, fakeMoney, - settings: { decks, hitSoft17, blackjackPayout, cutCardRatio, phaseDurations, animation }, - shoe: buildShoe(decks), - discard: [], - dealer: { cards: [], holeHidden: true }, - players: {}, // userId -> { id, globalName, avatar, bank, currentBet, inRound, hands: [{cards, stood, busted, doubled, surrendered, hasActed}], activeHand: 0 } - leavingAfterRound: {}, - }; + return { + id: "blackjack-room", + name: "Blackjack", + created_at: Date.now(), + status: "betting", // betting | dealing | playing | dealer | payout | shuffle + phase_ends_at: Date.now() + phaseDurations.bettingMs, + minBet, + maxBet, + fakeMoney, + settings: { + decks, + hitSoft17, + blackjackPayout, + cutCardRatio, + phaseDurations, + animation, + }, + shoe: buildShoe(decks), + discard: [], + dealer: { cards: [], holeHidden: true }, + players: {}, // userId -> { id, globalName, avatar, bank, currentBet, inRound, hands: [{cards, stood, busted, doubled, surrendered, hasActed}], activeHand: 0 } + leavingAfterRound: {}, + }; } // Reshuffle at start of the next round if the shoe is low export function needsReshuffle(room) { - return room.shoe.length < singleDeck.length * room.settings.decks * room.settings.cutCardRatio; + return room.shoe.length < singleDeck.length * room.settings.decks * room.settings.cutCardRatio; } // --- Round Lifecycle helpers --- export function resetForNewRound(room) { - room.status = "betting"; - room.dealer = { cards: [], holeHidden: true }; - room.leavingAfterRound = {}; - // Clear per-round attributes on players, but keep bank and presence - for (const p of Object.values(room.players)) { - p.inRound = false; - p.currentBet = 0; - p.hands = [ { cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 } ]; - p.activeHand = 0; - } + room.status = "betting"; + room.dealer = { cards: [], holeHidden: true }; + room.leavingAfterRound = {}; + // Clear per-round attributes on players, but keep bank and presence + for (const p of Object.values(room.players)) { + p.inRound = false; + p.currentBet = 0; + p.hands = [ + { + cards: [], + stood: false, + busted: false, + doubled: false, + surrendered: false, + hasActed: false, + bet: 0, + }, + ]; + p.activeHand = 0; + } } export function startBetting(room, now) { - resetForNewRound(room); - if (needsReshuffle(room)) { - room.status = "shuffle"; - // quick shuffle animation phase - room.shoe = buildShoe(room.settings.decks); - } - room.status = "betting"; - room.phase_ends_at = now + room.settings.phaseDurations.bettingMs; + resetForNewRound(room); + if (needsReshuffle(room)) { + room.status = "shuffle"; + // quick shuffle animation phase + room.shoe = buildShoe(room.settings.decks); + } + room.status = "betting"; + room.phase_ends_at = now + room.settings.phaseDurations.bettingMs; } export function dealInitial(room) { - room.status = "dealing"; - // Deal one to each player who placed a bet, then again, then dealer up + hole - const actives = Object.values(room.players).filter(p => p.currentBet >= room.minBet); - for (const p of actives) { - p.inRound = true; - p.hands = [ { cards: [draw(room.shoe)], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: p.currentBet } ]; - } - room.dealer.cards = [draw(room.shoe), draw(room.shoe)]; - room.dealer.holeHidden = true; - for (const p of actives) { - p.hands[0].cards.push(draw(room.shoe)); - } - room.status = "playing"; + room.status = "dealing"; + // Deal one to each player who placed a bet, then again, then dealer up + hole + const actives = Object.values(room.players).filter((p) => p.currentBet >= room.minBet); + for (const p of actives) { + p.inRound = true; + p.hands = [ + { + cards: [draw(room.shoe)], + stood: false, + busted: false, + doubled: false, + surrendered: false, + hasActed: false, + bet: p.currentBet, + }, + ]; + } + room.dealer.cards = [draw(room.shoe), draw(room.shoe)]; + room.dealer.holeHidden = true; + for (const p of actives) { + p.hands[0].cards.push(draw(room.shoe)); + } + room.status = "playing"; } export function autoActions(room) { - // Auto-stand if player already blackjack - for (const p of Object.values(room.players)) { - if (!p.inRound) continue; - const h = p.hands[p.activeHand]; - if (isBlackjack(h.cards)) { - h.stood = true; - h.hasActed = true; - } - } + // Auto-stand if player already blackjack + for (const p of Object.values(room.players)) { + if (!p.inRound) continue; + const h = p.hands[p.activeHand]; + if (isBlackjack(h.cards)) { + h.stood = true; + h.hasActed = true; + } + } } export function everyoneDone(room) { - return Object.values(room.players).every(p => { - if (!p.inRound) return true; - return p.hands.filter(h => !h.stood && !h.busted && !h.surrendered)?.length === 0; - }); + return Object.values(room.players).every((p) => { + if (!p.inRound) return true; + return p.hands.filter((h) => !h.stood && !h.busted && !h.surrendered)?.length === 0; + }); } export function dealerPlay(room) { - room.status = "dealer"; - room.dealer.holeHidden = false; - while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) { - room.dealer.cards.push(draw(room.shoe)); - } + room.status = "dealer"; + room.dealer.holeHidden = false; + while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) { + room.dealer.cards.push(draw(room.shoe)); + } } export async function settleAll(room) { - room.status = "payout"; - const allRes = {} - for (const p of Object.values(room.players)) { - if (!p.inRound) continue; - for (const hand of p.hands) { - const res = settleHand({ - bet: hand.bet, - playerCards: hand.cards, - dealerCards: room.dealer.cards, - doubled: hand.doubled, - surrendered: hand.surrendered, - blackjackPayout: room.settings.blackjackPayout, - }); - if (allRes[p.id]) { - allRes[p.id].push(res); - } else { - allRes[p.id] = [res]; - } + room.status = "payout"; + const allRes = {}; + for (const p of Object.values(room.players)) { + if (!p.inRound) continue; + for (const hand of p.hands) { + const res = settleHand({ + bet: hand.bet, + playerCards: hand.cards, + dealerCards: room.dealer.cards, + doubled: hand.doubled, + surrendered: hand.surrendered, + blackjackPayout: room.settings.blackjackPayout, + }); + if (allRes[p.id]) { + allRes[p.id].push(res); + } else { + allRes[p.id] = [res]; + } - p.totalDelta += res.delta - p.totalBets++ - if (res.result === 'win' || res.result === 'push' || res.result === 'blackjack') { - const userDB = getUser.get(p.id); - if (userDB) { - const coins = userDB.coins; - try { - updateUserCoins.run({ id: p.id, coins: coins + hand.bet + res.delta }); - insertLog.run({ - id: `${p.id}-blackjack-${Date.now()}`, - user_id: p.id, target_user_id: null, - action: 'BLACKJACK_PAYOUT', - coins_amount: res.delta + hand.bet, user_new_amount: coins + hand.bet + res.delta, - }); - p.bank = coins + hand.bet + res.delta - } catch (e) { - console.log(e) - } - } - } - emitToast({ type: `payout-res`, allRes }); - hand.result = res.result; - hand.delta = res.delta; - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const msg = await generalChannel.messages.fetch(p.msgId); - const updatedEmbed = new EmbedBuilder() - .setDescription(`<@${p.id}> joue au Blackjack.`) - .addFields( - { - name: `Gains`, - value: `**${p.totalDelta >= 0 ? '+' + p.totalDelta : p.totalDelta}** Flopos`, - inline: true - }, - { - name: `Mises jouées`, - value: `**${p.totalBets}**`, - inline: true - } - ) - .setColor(p.totalDelta >= 0 ? 0x22A55B : 0xED4245) - .setTimestamp(new Date()); - await msg.edit({ embeds: [updatedEmbed], components: [] }); - } catch (e) { - console.log(e); - } - } - } + p.totalDelta += res.delta; + p.totalBets++; + if (res.result === "win" || res.result === "push" || res.result === "blackjack") { + const userDB = getUser.get(p.id); + if (userDB) { + const coins = userDB.coins; + try { + updateUserCoins.run({ + id: p.id, + coins: coins + hand.bet + res.delta, + }); + insertLog.run({ + id: `${p.id}-blackjack-${Date.now()}`, + user_id: p.id, + target_user_id: null, + action: "BLACKJACK_PAYOUT", + coins_amount: res.delta + hand.bet, + user_new_amount: coins + hand.bet + res.delta, + }); + p.bank = coins + hand.bet + res.delta; + } catch (e) { + console.log(e); + } + } + } + emitToast({ type: `payout-res`, allRes }); + hand.result = res.result; + hand.delta = res.delta; + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const msg = await generalChannel.messages.fetch(p.msgId); + const updatedEmbed = new EmbedBuilder() + .setDescription(`<@${p.id}> joue au Blackjack.`) + .addFields( + { + name: `Gains`, + value: `**${p.totalDelta >= 0 ? "+" + p.totalDelta : p.totalDelta}** Flopos`, + inline: true, + }, + { + name: `Mises jouées`, + value: `**${p.totalBets}**`, + inline: true, + }, + ) + .setColor(p.totalDelta >= 0 ? 0x22a55b : 0xed4245) + .setTimestamp(new Date()); + await msg.edit({ embeds: [updatedEmbed], components: [] }); + } catch (e) { + console.log(e); + } + } + } } // Apply a player decision; returns a string event or throws on invalid. export function applyAction(room, playerId, action) { - const p = room.players[playerId]; - if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed"); - const hand = p.hands[p.activeHand]; + const p = room.players[playerId]; + if (!p || !p.inRound || room.status !== "playing") throw new Error("Not allowed"); + const hand = p.hands[p.activeHand]; - switch (action) { - case "hit": { - if (hand.stood || hand.busted) throw new Error("Already ended"); - hand.hasActed = true; - hand.cards.push(draw(room.shoe)); - if (isBust(hand.cards)) hand.busted = true; - return "hit"; - } - case "stand": { - hand.stood = true; - hand.hasActed = true; - p.activeHand++; - return "stand"; - } - case "double": { - if (!canDouble(hand)) throw new Error("Cannot double now"); - hand.doubled = true; - hand.bet*=2 - p.currentBet+=hand.bet/2 - hand.hasActed = true; - // The caller (routes) must also handle additional balance lock on the bet if using real coins - hand.cards.push(draw(room.shoe)); - if (isBust(hand.cards)) hand.busted = true; - else hand.stood = true; - p.activeHand++; - return "double"; - } - case "split": { - if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards"); - const r0 = hand.cards[0][0]; - const r1 = hand.cards[1][0]; - if (r0 !== r1) throw new Error("Cannot split: cards not same rank"); + switch (action) { + case "hit": { + if (hand.stood || hand.busted) throw new Error("Already ended"); + hand.hasActed = true; + hand.cards.push(draw(room.shoe)); + if (isBust(hand.cards)) hand.busted = true; + return "hit"; + } + case "stand": { + hand.stood = true; + hand.hasActed = true; + p.activeHand++; + return "stand"; + } + case "double": { + if (!canDouble(hand)) throw new Error("Cannot double now"); + hand.doubled = true; + hand.bet *= 2; + p.currentBet += hand.bet / 2; + hand.hasActed = true; + // The caller (routes) must also handle additional balance lock on the bet if using real coins + hand.cards.push(draw(room.shoe)); + if (isBust(hand.cards)) hand.busted = true; + else hand.stood = true; + p.activeHand++; + return "double"; + } + case "split": { + if (hand.cards.length !== 2) throw new Error("Cannot split: not exactly 2 cards"); + const r0 = hand.cards[0][0]; + const r1 = hand.cards[1][0]; + if (r0 !== r1) throw new Error("Cannot split: cards not same rank"); - const cardA = hand.cards[0]; - const cardB = hand.cards[1]; + const cardA = hand.cards[0]; + const cardB = hand.cards[1]; - hand.cards = [cardA]; - hand.stood = false; - hand.busted = false; - hand.doubled = false; - hand.surrendered = false; - hand.hasActed = false; + hand.cards = [cardA]; + hand.stood = false; + hand.busted = false; + hand.doubled = false; + hand.surrendered = false; + hand.hasActed = false; - const newHand = { - cards: [cardB], - stood: false, - busted: false, - doubled: false, - surrendered: false, - hasActed: false, - bet: hand.bet, - } + const newHand = { + cards: [cardB], + stood: false, + busted: false, + doubled: false, + surrendered: false, + hasActed: false, + bet: hand.bet, + }; - p.currentBet *= 2 + p.currentBet *= 2; - p.hands.splice(p.activeHand + 1, 0, newHand); + p.hands.splice(p.activeHand + 1, 0, newHand); - hand.cards.push(draw(room.shoe)); - newHand.cards.push(draw(room.shoe)); + hand.cards.push(draw(room.shoe)); + newHand.cards.push(draw(room.shoe)); - return "split"; - } - default: - throw new Error("Invalid action"); - } -} \ No newline at end of file + return "split"; + } + default: + throw new Error("Invalid action"); + } +} diff --git a/src/game/elo.js b/src/game/elo.js index c720007..6eba62b 100644 --- a/src/game/elo.js +++ b/src/game/elo.js @@ -1,12 +1,6 @@ -import { - getUser, - getUserElo, - insertElos, - updateElo, - insertGame, -} from '../database/index.js'; -import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js"; -import {client} from "../bot/client.js"; +import { getUser, getUserElo, insertElos, updateElo, insertGame } from "../database/index.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { client } from "../bot/client.js"; /** * Handles Elo calculation for a standard 1v1 game. @@ -17,81 +11,85 @@ import {client} from "../bot/client.js"; * @param {string} type - The type of game being played (e.g., 'TICTACTOE', 'CONNECT4'). */ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) { - // --- 1. Fetch Player Data --- - const p1DB = getUser.get(p1Id); - const p2DB = getUser.get(p2Id); - if (!p1DB || !p2DB) { - console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`); - return; - } + // --- 1. Fetch Player Data --- + const p1DB = getUser.get(p1Id); + const p2DB = getUser.get(p2Id); + if (!p1DB || !p2DB) { + console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`); + return; + } - let p1EloData = getUserElo.get({ id: p1Id }); - let p2EloData = getUserElo.get({ id: p2Id }); + let p1EloData = getUserElo.get({ id: p1Id }); + let p2EloData = getUserElo.get({ id: p2Id }); - // --- 2. Initialize Elo if it doesn't exist --- - if (!p1EloData) { - await insertElos.run({ id: p1Id, elo: 1000 }); - p1EloData = { id: p1Id, elo: 1000 }; - } - if (!p2EloData) { - await insertElos.run({ id: p2Id, elo: 1000 }); - p2EloData = { id: p2Id, elo: 1000 }; - } + // --- 2. Initialize Elo if it doesn't exist --- + if (!p1EloData) { + await insertElos.run({ id: p1Id, elo: 1000 }); + p1EloData = { id: p1Id, elo: 1000 }; + } + if (!p2EloData) { + await insertElos.run({ id: p2Id, elo: 1000 }); + p2EloData = { id: p2Id, elo: 1000 }; + } - const p1CurrentElo = p1EloData.elo; - const p2CurrentElo = p2EloData.elo; + const p1CurrentElo = p1EloData.elo; + const p2CurrentElo = p2EloData.elo; - // --- 3. Calculate Elo Change --- - // The K-factor determines how much the Elo rating changes after a game. - const K_FACTOR = 32; + // --- 3. Calculate Elo Change --- + // The K-factor determines how much the Elo rating changes after a game. + const K_FACTOR = 32; - // Calculate expected scores - const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400)); - const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400)); + // Calculate expected scores + const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400)); + const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400)); - // Calculate new Elo ratings - const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1)); - const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2)); + // Calculate new Elo ratings + const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1)); + const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2)); - // Ensure Elo doesn't drop below a certain threshold (e.g., 100) - const finalP1Elo = Math.max(0, p1NewElo); - const finalP2Elo = Math.max(0, p2NewElo); + // Ensure Elo doesn't drop below a certain threshold (e.g., 100) + const finalP1Elo = Math.max(0, p1NewElo); + const finalP2Elo = Math.max(0, p2NewElo); - console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`); - console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`); - try { - const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); - const user1 = await client.users.fetch(p1Id); - const user2 = await client.users.fetch(p2Id); - const diff1 = finalP1Elo - p1CurrentElo; - const diff2 = finalP2Elo - p2CurrentElo; - const embed = new EmbedBuilder() - .setTitle(`FlopoRank - ${type}`) - .setDescription(` - **${user1.globalName || user1.username}** a ${diff1 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? '↗️' : '↘️'} **${finalP1Elo}**\n - **${user2.globalName || user2.username}** a ${diff2 > 0 ? 'gagné' : 'perdu'} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? '↗️' : '↘️'} **${finalP2Elo}**\n - `) - .setColor('#5865f2'); - await generalChannel.send({ embeds: [embed] }); - } catch (e) { console.error(`Failed to post elo update message`, e); } + console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`); + console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`); + try { + const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); + const user1 = await client.users.fetch(p1Id); + const user2 = await client.users.fetch(p2Id); + const diff1 = finalP1Elo - p1CurrentElo; + const diff2 = finalP2Elo - p2CurrentElo; + const embed = new EmbedBuilder() + .setTitle(`FlopoRank - ${type}`) + .setDescription( + ` + **${user1.globalName || user1.username}** a ${diff1 > 0 ? "gagné" : "perdu"} **${Math.abs(diff1)}** elo 🏆 ${p1CurrentElo} ${diff1 > 0 ? "↗️" : "↘️"} **${finalP1Elo}**\n + **${user2.globalName || user2.username}** a ${diff2 > 0 ? "gagné" : "perdu"} **${Math.abs(diff2)}** elo 🏆 ${p2CurrentElo} ${diff2 > 0 ? "↗️" : "↘️"} **${finalP2Elo}**\n + `, + ) + .setColor("#5865f2"); + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.error(`Failed to post elo update message`, e); + } - // --- 4. Update Database --- - updateElo.run({ id: p1Id, elo: finalP1Elo }); - updateElo.run({ id: p2Id, elo: finalP2Elo }); + // --- 4. Update Database --- + updateElo.run({ id: p1Id, elo: finalP1Elo }); + updateElo.run({ id: p2Id, elo: finalP2Elo }); - insertGame.run({ - id: `${p1Id}-${p2Id}-${Date.now()}`, - p1: p1Id, - p2: p2Id, - p1_score: p1Score, - p2_score: p2Score, - p1_elo: p1CurrentElo, - p2_elo: p2CurrentElo, - p1_new_elo: finalP1Elo, - p2_new_elo: finalP2Elo, - type: type, - timestamp: Date.now(), - }); + insertGame.run({ + id: `${p1Id}-${p2Id}-${Date.now()}`, + p1: p1Id, + p2: p2Id, + p1_score: p1Score, + p2_score: p2Score, + p1_elo: p1CurrentElo, + p2_elo: p2CurrentElo, + p1_new_elo: finalP1Elo, + p2_new_elo: finalP2Elo, + type: type, + timestamp: Date.now(), + }); } /** @@ -99,64 +97,66 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) { * @param {object} room - The poker room object containing player and winner info. */ export async function pokerEloHandler(room) { - if (room.fakeMoney) { - console.log("Skipping Elo update for fake money poker game."); - return; - } + if (room.fakeMoney) { + console.log("Skipping Elo update for fake money poker game."); + return; + } - const playerIds = Object.keys(room.players); - if (playerIds.length < 2) return; // Not enough players to calculate Elo + const playerIds = Object.keys(room.players); + if (playerIds.length < 2) return; // Not enough players to calculate Elo - // Fetch all players' Elo data at once - const dbPlayers = playerIds.map(id => { - const user = getUser.get(id); - const elo = getUserElo.get({ id })?.elo || 1000; - return { ...user, elo }; - }); + // Fetch all players' Elo data at once + const dbPlayers = playerIds.map((id) => { + const user = getUser.get(id); + const elo = getUserElo.get({ id })?.elo || 1000; + return { ...user, elo }; + }); - const winnerIds = new Set(room.winners); - const playerCount = dbPlayers.length; - const K_BASE = 16; // A lower K-factor is often used for multi-player games + const winnerIds = new Set(room.winners); + const playerCount = dbPlayers.length; + const K_BASE = 16; // A lower K-factor is often used for multi-player games - const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount; + const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount; - dbPlayers.forEach(player => { - // Expected score is the chance of winning against an "average" player from the field - const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400)); + dbPlayers.forEach((player) => { + // Expected score is the chance of winning against an "average" player from the field + const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400)); - // Determine actual score - let actualScore; - if (winnerIds.has(player.id)) { - // Winners share the "win" points - actualScore = 1 / winnerIds.size; - } else { - actualScore = 0; - } + // Determine actual score + let actualScore; + if (winnerIds.has(player.id)) { + // Winners share the "win" points + actualScore = 1 / winnerIds.size; + } else { + actualScore = 0; + } - // Dynamic K-factor: higher impact for more significant results - const kFactor = K_BASE * playerCount; - const eloChange = kFactor * (actualScore - expectedScore); - const newElo = Math.max(100, Math.round(player.elo + eloChange)); + // Dynamic K-factor: higher impact for more significant results + const kFactor = K_BASE * playerCount; + const eloChange = kFactor * (actualScore - expectedScore); + const newElo = Math.max(100, Math.round(player.elo + eloChange)); - if (!isNaN(newElo)) { - console.log(`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`); - updateElo.run({ id: player.id, elo: newElo }); + if (!isNaN(newElo)) { + console.log( + `Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`, + ); + updateElo.run({ id: player.id, elo: newElo }); - insertGame.run({ - id: `${player.id}-poker-${Date.now()}`, - p1: player.id, - p2: null, // No single opponent - p1_score: actualScore, - p2_score: null, - p1_elo: player.elo, - p2_elo: Math.round(averageElo), // Log the average opponent Elo for context - p1_new_elo: newElo, - p2_new_elo: null, - type: 'POKER_ROUND', - timestamp: Date.now(), - }); - } else { - console.error(`Error calculating new Elo for ${player.globalName}.`); - } - }); -} \ No newline at end of file + insertGame.run({ + id: `${player.id}-poker-${Date.now()}`, + p1: player.id, + p2: null, // No single opponent + p1_score: actualScore, + p2_score: null, + p1_elo: player.elo, + p2_elo: Math.round(averageElo), // Log the average opponent Elo for context + p1_new_elo: newElo, + p2_new_elo: null, + type: "POKER_ROUND", + timestamp: Date.now(), + }); + } else { + console.error(`Error calculating new Elo for ${player.globalName}.`); + } + }); +} diff --git a/src/game/points.js b/src/game/points.js index 5cf480e..69b59cc 100644 --- a/src/game/points.js +++ b/src/game/points.js @@ -1,14 +1,16 @@ import { - getUser, - updateUserCoins, - insertLog, - getAllSkins, - insertSOTD, - clearSOTDStats, - getAllSOTDStats, deleteSOTD, insertGame, -} from '../database/index.js'; -import { messagesTimestamps, activeSlowmodes, skins } from './state.js'; -import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js'; + getUser, + updateUserCoins, + insertLog, + getAllSkins, + insertSOTD, + clearSOTDStats, + getAllSOTDStats, + deleteSOTD, + insertGame, +} from "../database/index.js"; +import { messagesTimestamps, activeSlowmodes, skins } from "./state.js"; +import { deal, createSeededRNG, seededShuffle, createDeck } from "./solitaire.js"; /** * Handles awarding points (coins) to users for their message activity. @@ -17,53 +19,53 @@ import { deal, createSeededRNG, seededShuffle, createDeck } from './solitaire.js * @returns {boolean} True if points were awarded, false otherwise. */ export async function channelPointsHandler(message) { - const author = message.author; - const authorDB = getUser.get(author.id); + const author = message.author; + const authorDB = getUser.get(author.id); - if (!authorDB) { - // User not in our database, do nothing. - return false; - } + if (!authorDB) { + // User not in our database, do nothing. + return false; + } - // Ignore short messages or commands that might be spammed - if (message.content.length < 3 || message.content.startsWith('?')) { - return false; - } + // Ignore short messages or commands that might be spammed + if (message.content.length < 3 || message.content.startsWith("?")) { + return false; + } - const now = Date.now(); - const userTimestamps = messagesTimestamps.get(author.id) || []; + const now = Date.now(); + const userTimestamps = messagesTimestamps.get(author.id) || []; - // Filter out timestamps older than 15 minutes (900,000 ms) - const recentTimestamps = userTimestamps.filter(ts => now - ts < 900000); + // Filter out timestamps older than 15 minutes (900,000 ms) + const recentTimestamps = userTimestamps.filter((ts) => now - ts < 900000); - // If the user has already sent 10 messages in the last 15 mins, do nothing - if (recentTimestamps.length >= 10) { - return false; - } + // If the user has already sent 10 messages in the last 15 mins, do nothing + if (recentTimestamps.length >= 10) { + return false; + } - // Add the new message timestamp - recentTimestamps.push(now); - messagesTimestamps.set(author.id, recentTimestamps); + // Add the new message timestamp + recentTimestamps.push(now); + messagesTimestamps.set(author.id, recentTimestamps); - // Award 50 coins for the 10th message, 10 for others - const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10; - const newCoinTotal = authorDB.coins + coinsToAdd; + // Award 50 coins for the 10th message, 10 for others + const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10; + const newCoinTotal = authorDB.coins + coinsToAdd; - updateUserCoins.run({ - id: author.id, - coins: newCoinTotal, - }); + updateUserCoins.run({ + id: author.id, + coins: newCoinTotal, + }); - insertLog.run({ - id: `${author.id}-${now}`, - user_id: author.id, - action: 'AUTO_COINS', - target_user_id: null, - coins_amount: coinsToAdd, - user_new_amount: newCoinTotal, - }); + insertLog.run({ + id: `${author.id}-${now}`, + user_id: author.id, + action: "AUTO_COINS", + target_user_id: null, + coins_amount: coinsToAdd, + user_new_amount: newCoinTotal, + }); - return true; // Indicate that points were awarded + return true; // Indicate that points were awarded } /** @@ -72,37 +74,37 @@ export async function channelPointsHandler(message) { * @returns {object} An object indicating if a message was deleted or a slowmode expired. */ export async function slowmodesHandler(message) { - const author = message.author; - const authorSlowmode = activeSlowmodes[author.id]; + const author = message.author; + const authorSlowmode = activeSlowmodes[author.id]; - if (!authorSlowmode) { - return { deleted: false, expired: false }; - } + if (!authorSlowmode) { + return { deleted: false, expired: false }; + } - const now = Date.now(); + const now = Date.now(); - // Check if the slowmode duration has passed - if (now > authorSlowmode.endAt) { - console.log(`Slowmode for ${author.username} has expired.`); - delete activeSlowmodes[author.id]; - return { deleted: false, expired: true }; - } + // Check if the slowmode duration has passed + if (now > authorSlowmode.endAt) { + console.log(`Slowmode for ${author.username} has expired.`); + delete activeSlowmodes[author.id]; + return { deleted: false, expired: true }; + } - // Check if the user is messaging too quickly (less than 1 minute between messages) - if (authorSlowmode.lastMessage && (now - authorSlowmode.lastMessage < 60 * 1000)) { - try { - await message.delete(); - console.log(`Deleted a message from slowmoded user: ${author.username}`); - return { deleted: true, expired: false }; - } catch (err) { - console.error(`Failed to delete slowmode message:`, err); - return { deleted: false, expired: false }; - } - } else { - // Update the last message timestamp for the user - authorSlowmode.lastMessage = now; - return { deleted: false, expired: false }; - } + // Check if the user is messaging too quickly (less than 1 minute between messages) + if (authorSlowmode.lastMessage && now - authorSlowmode.lastMessage < 60 * 1000) { + try { + await message.delete(); + console.log(`Deleted a message from slowmoded user: ${author.username}`); + return { deleted: true, expired: false }; + } catch (err) { + console.error(`Failed to delete slowmode message:`, err); + return { deleted: false, expired: false }; + } + } else { + // Update the last message timestamp for the user + authorSlowmode.lastMessage = now; + return { deleted: false, expired: false }; + } } /** @@ -111,27 +113,27 @@ export async function slowmodesHandler(message) { * @returns {string} The calculated random price as a string. */ export function randomSkinPrice() { - const dbSkins = getAllSkins.all(); - if (dbSkins.length === 0) return '0.00'; + const dbSkins = getAllSkins.all(); + if (dbSkins.length === 0) return "0.00"; - const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)]; - const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid); + const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)]; + const randomSkinData = skins.find((skin) => skin.uuid === randomDbSkin.uuid); - if (!randomSkinData) return '0.00'; + if (!randomSkinData) return "0.00"; - // Generate random level and chroma - const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; - let randomChroma = 1; - if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { - randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; - } + // Generate random level and chroma + const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; + let randomChroma = 1; + if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { + randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; + } - // Calculate price based on these random values - let result = parseFloat(randomDbSkin.basePrice); - result *= (1 + (randomLevel / Math.max(randomSkinData.levels.length, 2))); - result *= (1 + (randomChroma / 4)); + // Calculate price based on these random values + let result = parseFloat(randomDbSkin.basePrice); + result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2); + result *= 1 + randomChroma / 4; - return result.toFixed(0); + return result.toFixed(0); } /** @@ -139,68 +141,70 @@ export function randomSkinPrice() { * This function clears previous stats, awards the winner, and generates a new daily seed. */ export function initTodaysSOTD() { - console.log('Initializing new Solitaire of the Day...'); + console.log("Initializing new Solitaire of the Day..."); - // 1. Award previous day's winner - const rankings = getAllSOTDStats.all(); - if (rankings.length > 0) { - const winnerId = rankings[0].user_id; - const winnerUser = getUser.get(winnerId); + // 1. Award previous day's winner + const rankings = getAllSOTDStats.all(); + if (rankings.length > 0) { + const winnerId = rankings[0].user_id; + const winnerUser = getUser.get(winnerId); - if (winnerUser) { - const reward = 1000; - const newCoinTotal = winnerUser.coins + reward; - updateUserCoins.run({ id: winnerId, coins: newCoinTotal }); - insertLog.run({ - id: `${winnerId}-sotd-win-${Date.now()}`, - target_user_id: null, - user_id: winnerId, - action: 'SOTD_FIRST_PLACE', - coins_amount: reward, - user_new_amount: newCoinTotal, - }); - console.log(`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`); - insertGame.run({ - id: `${winnerId}-${Date.now()}`, - p1: winnerId, - p2: null, - p1_score: rankings[0].score, - p2_score: null, - p1_elo: winnerUser.elo, - p2_elo: null, - p1_new_elo: winnerUser.elo, - p2_new_elo: null, - type: 'SOTD', - timestamp: Date.now(), - }); - } - } + if (winnerUser) { + const reward = 1000; + const newCoinTotal = winnerUser.coins + reward; + updateUserCoins.run({ id: winnerId, coins: newCoinTotal }); + insertLog.run({ + id: `${winnerId}-sotd-win-${Date.now()}`, + target_user_id: null, + user_id: winnerId, + action: "SOTD_FIRST_PLACE", + coins_amount: reward, + user_new_amount: newCoinTotal, + }); + console.log( + `${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`, + ); + insertGame.run({ + id: `${winnerId}-${Date.now()}`, + p1: winnerId, + p2: null, + p1_score: rankings[0].score, + p2_score: null, + p1_elo: winnerUser.elo, + p2_elo: null, + p1_new_elo: winnerUser.elo, + p2_new_elo: null, + type: "SOTD", + timestamp: Date.now(), + }); + } + } - // 2. Generate a new seeded deck for today - const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2); - let numericSeed = 0; - for (let i = 0; i < newRandomSeed.length; i++) { - numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xFFFFFFFF; - } + // 2. Generate a new seeded deck for today + const newRandomSeed = Date.now().toString(36) + Math.random().toString(36).substr(2); + let numericSeed = 0; + for (let i = 0; i < newRandomSeed.length; i++) { + numericSeed = (numericSeed + newRandomSeed.charCodeAt(i)) & 0xffffffff; + } - const rng = createSeededRNG(numericSeed); - const deck = createDeck(); - const shuffledDeck = seededShuffle(deck, rng); - const todaysSOTD = deal(shuffledDeck); + const rng = createSeededRNG(numericSeed); + const deck = createDeck(); + const shuffledDeck = seededShuffle(deck, rng); + const todaysSOTD = deal(shuffledDeck); - // 3. Clear old stats and save the new game state to the database - try { - clearSOTDStats.run(); - deleteSOTD.run(); - insertSOTD.run({ - tableauPiles: JSON.stringify(todaysSOTD.tableauPiles), - foundationPiles: JSON.stringify(todaysSOTD.foundationPiles), - stockPile: JSON.stringify(todaysSOTD.stockPile), - wastePile: JSON.stringify(todaysSOTD.wastePile), - seed: newRandomSeed, - }); - console.log("Today's SOTD is ready with a new seed."); - } catch(e) { - console.error("Error saving new SOTD to database:", e); - } -} \ No newline at end of file + // 3. Clear old stats and save the new game state to the database + try { + clearSOTDStats.run(); + deleteSOTD.run(); + insertSOTD.run({ + tableauPiles: JSON.stringify(todaysSOTD.tableauPiles), + foundationPiles: JSON.stringify(todaysSOTD.foundationPiles), + stockPile: JSON.stringify(todaysSOTD.stockPile), + wastePile: JSON.stringify(todaysSOTD.wastePile), + seed: newRandomSeed, + }); + console.log("Today's SOTD is ready with a new seed."); + } catch (e) { + console.error("Error saving new SOTD to database:", e); + } +} diff --git a/src/game/poker.js b/src/game/poker.js index 1a26263..bd64861 100644 --- a/src/game/poker.js +++ b/src/game/poker.js @@ -1,12 +1,60 @@ -import pkg from 'pokersolver'; +import pkg from "pokersolver"; const { Hand } = pkg; // An array of all 52 standard playing cards. export const initialCards = [ - 'Ad', '2d', '3d', '4d', '5d', '6d', '7d', '8d', '9d', 'Td', 'Jd', 'Qd', 'Kd', - 'As', '2s', '3s', '4s', '5s', '6s', '7s', '8s', '9s', 'Ts', 'Js', 'Qs', 'Ks', - 'Ac', '2c', '3c', '4c', '5c', '6c', '7c', '8c', '9c', 'Tc', 'Jc', 'Qc', 'Kc', - 'Ah', '2h', '3h', '4h', '5h', '6h', '7h', '8h', '9h', 'Th', 'Jh', 'Qh', 'Kh', + "Ad", + "2d", + "3d", + "4d", + "5d", + "6d", + "7d", + "8d", + "9d", + "Td", + "Jd", + "Qd", + "Kd", + "As", + "2s", + "3s", + "4s", + "5s", + "6s", + "7s", + "8s", + "9s", + "Ts", + "Js", + "Qs", + "Ks", + "Ac", + "2c", + "3c", + "4c", + "5c", + "6c", + "7c", + "8c", + "9c", + "Tc", + "Jc", + "Qc", + "Kc", + "Ah", + "2h", + "3h", + "4h", + "5h", + "6h", + "7h", + "8h", + "9h", + "Th", + "Jh", + "Qh", + "Kh", ]; /** @@ -14,8 +62,8 @@ export const initialCards = [ * @returns {Array} A new array containing all 52 cards in a random order. */ export function initialShuffledCards() { - // Create a copy and sort it randomly - return [...initialCards].sort(() => 0.5 - Math.random()); + // Create a copy and sort it randomly + return [...initialCards].sort(() => 0.5 - Math.random()); } /** @@ -25,19 +73,19 @@ export function initialShuffledCards() { * @returns {string|null} The ID of the next player, or null if none is found. */ export function getFirstActivePlayerAfterDealer(room) { - const players = Object.values(room.players); - const dealerPosition = players.findIndex((p) => p.id === room.dealer); + const players = Object.values(room.players); + const dealerPosition = players.findIndex((p) => p.id === room.dealer); - // Loop through players starting from the one after the dealer - for (let i = 1; i <= players.length; i++) { - const nextPos = (dealerPosition + i) % players.length; - const nextPlayer = players[nextPos]; - // Player must not be folded or all-in to be able to act - if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) { - return nextPlayer.id; - } - } - return null; // Should not happen in a normal game + // Loop through players starting from the one after the dealer + for (let i = 1; i <= players.length; i++) { + const nextPos = (dealerPosition + i) % players.length; + const nextPlayer = players[nextPos]; + // Player must not be folded or all-in to be able to act + if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) { + return nextPlayer.id; + } + } + return null; // Should not happen in a normal game } /** @@ -46,18 +94,18 @@ export function getFirstActivePlayerAfterDealer(room) { * @returns {string|null} The ID of the next player, or null if none is found. */ export function getNextActivePlayer(room) { - const players = Object.values(room.players); - const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player); + const players = Object.values(room.players); + const currentPlayerPosition = players.findIndex((p) => p.id === room.current_player); - // Loop through players starting from the one after the current player - for (let i = 1; i <= players.length; i++) { - const nextPos = (currentPlayerPosition + i) % players.length; - const nextPlayer = players[nextPos]; - if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) { - return nextPlayer.id; - } - } - return null; + // Loop through players starting from the one after the current player + for (let i = 1; i <= players.length; i++) { + const nextPos = (currentPlayerPosition + i) % players.length; + const nextPlayer = players[nextPos]; + if (nextPlayer && !nextPlayer.folded && !nextPlayer.allin) { + return nextPlayer.id; + } + } + return null; } /** @@ -66,40 +114,54 @@ export function getNextActivePlayer(room) { * @returns {object} An object with `endRound`, `winner`, and `nextPhase` properties. */ export function checkEndOfBettingRound(room) { - const activePlayers = Object.values(room.players).filter((p) => !p.folded); + const activePlayers = Object.values(room.players).filter((p) => !p.folded); - // --- Scenario 1: Only one player left (everyone else folded) --- - if (activePlayers.length === 1) { - return { endRound: true, winner: activePlayers[0].id, nextPhase: 'showdown' }; - } + // --- Scenario 1: Only one player left (everyone else folded) --- + if (activePlayers.length === 1) { + return { + endRound: true, + winner: activePlayers[0].id, + nextPhase: "showdown", + }; + } - // --- Scenario 2: All remaining players are all-in --- - // The hand goes immediately to a "progressive showdown". - const allInPlayers = activePlayers.filter(p => p.allin); - if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) { - return { endRound: true, winner: null, nextPhase: 'progressive-showdown' }; - } + // --- Scenario 2: All remaining players are all-in --- + // The hand goes immediately to a "progressive showdown". + const allInPlayers = activePlayers.filter((p) => p.allin); + if (allInPlayers.length >= 2 && allInPlayers.length === activePlayers.length) { + return { endRound: true, winner: null, nextPhase: "progressive-showdown" }; + } - // --- Scenario 3: All active players have acted and bets are equal --- - const allBetsMatched = activePlayers.every(p => - p.allin || // Player is all-in - (p.bet === room.highest_bet && p.last_played_turn === room.current_turn) // Or their bet matches the highest and they've acted this turn - ); + // --- Scenario 3: All active players have acted and bets are equal --- + const allBetsMatched = activePlayers.every( + (p) => + p.allin || // Player is all-in + (p.bet === room.highest_bet && p.last_played_turn === room.current_turn), // Or their bet matches the highest and they've acted this turn + ); - if (allBetsMatched) { - let nextPhase; - switch (room.current_turn) { - case 0: nextPhase = 'flop'; break; - case 1: nextPhase = 'turn'; break; - case 2: nextPhase = 'river'; break; - case 3: nextPhase = 'showdown'; break; - default: nextPhase = null; // Should not happen - } - return { endRound: true, winner: null, nextPhase: nextPhase }; - } + if (allBetsMatched) { + let nextPhase; + switch (room.current_turn) { + case 0: + nextPhase = "flop"; + break; + case 1: + nextPhase = "turn"; + break; + case 2: + nextPhase = "river"; + break; + case 3: + nextPhase = "showdown"; + break; + default: + nextPhase = null; // Should not happen + } + return { endRound: true, winner: null, nextPhase: nextPhase }; + } - // --- Default: The round continues --- - return { endRound: false, winner: null, nextPhase: null }; + // --- Default: The round continues --- + return { endRound: false, winner: null, nextPhase: null }; } /** @@ -108,32 +170,35 @@ export function checkEndOfBettingRound(room) { * @returns {Array} An array of winner IDs. Can contain multiple IDs in case of a split pot. */ export function checkRoomWinners(room) { - const communityCards = room.tapis; - const activePlayers = Object.values(room.players).filter(p => !p.folded); + const communityCards = room.tapis; + const activePlayers = Object.values(room.players).filter((p) => !p.folded); - // Solve each player's hand to find the best possible 5-card combination - const playerSolutions = activePlayers.map(player => ({ - id: player.id, - solution: Hand.solve([...communityCards, ...player.hand]), - })); + // Solve each player's hand to find the best possible 5-card combination + const playerSolutions = activePlayers.map((player) => ({ + id: player.id, + solution: Hand.solve([...communityCards, ...player.hand]), + })); - if (playerSolutions.length === 0) return []; + if (playerSolutions.length === 0) return []; - // Use pokersolver's `Hand.winners()` to find the best hand(s) - const winningSolutions = Hand.winners(playerSolutions.map(ps => ps.solution)); + // Use pokersolver's `Hand.winners()` to find the best hand(s) + const winningSolutions = Hand.winners(playerSolutions.map((ps) => ps.solution)); - // Find the player IDs that correspond to the winning hand solutions - const winnerIds = []; - for (const winningHand of winningSolutions) { - for (const playerSol of playerSolutions) { - // Compare description and card pool to uniquely identify the hand - if (playerSol.solution.descr === winningHand.descr && playerSol.solution.cardPool.toString() === winningHand.cardPool.toString()) { - if (!winnerIds.includes(playerSol.id)) { - winnerIds.push(playerSol.id); - } - } - } - } + // Find the player IDs that correspond to the winning hand solutions + const winnerIds = []; + for (const winningHand of winningSolutions) { + for (const playerSol of playerSolutions) { + // Compare description and card pool to uniquely identify the hand + if ( + playerSol.solution.descr === winningHand.descr && + playerSol.solution.cardPool.toString() === winningHand.cardPool.toString() + ) { + if (!winnerIds.includes(playerSol.id)) { + winnerIds.push(playerSol.id); + } + } + } + } - return winnerIds; -} \ No newline at end of file + return winnerIds; +} diff --git a/src/game/solitaire.js b/src/game/solitaire.js index a5f76eb..ceeb2f4 100644 --- a/src/game/solitaire.js +++ b/src/game/solitaire.js @@ -1,9 +1,9 @@ // --- Constants for Deck Creation --- -import {sleep} from "openai/core"; -import {emitSolitaireUpdate, emitUpdate} from "../server/socket.js"; +import { sleep } from "openai/core"; +import { emitSolitaireUpdate, emitUpdate } from "../server/socket.js"; -const SUITS = ['h', 'd', 's', 'c']; // Hearts, Diamonds, Spades, Clubs -const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K']; +const SUITS = ["h", "d", "s", "c"]; // Hearts, Diamonds, Spades, Clubs +const RANKS = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K"]; // --- Helper Functions for Card Logic --- @@ -13,12 +13,12 @@ const RANKS = ['A', '2', '3', '4', '5', '6', '7', '8', '9', 'T', 'J', 'Q', 'K']; * @returns {number} The numeric value (Ace=1, King=13). */ function getRankValue(rank) { - if (rank === 'A') return 1; - if (rank === 'T') return 10; - if (rank === 'J') return 11; - if (rank === 'Q') return 12; - if (rank === 'K') return 13; - return parseInt(rank, 10); + if (rank === "A") return 1; + if (rank === "T") return 10; + if (rank === "J") return 11; + if (rank === "Q") return 12; + if (rank === "K") return 13; + return parseInt(rank, 10); } /** @@ -27,10 +27,9 @@ function getRankValue(rank) { * @returns {string} 'red' or 'black'. */ function getCardColor(suit) { - return (suit === 'h' || suit === 'd') ? 'red' : 'black'; + return suit === "h" || suit === "d" ? "red" : "black"; } - // --- Core Game Logic Functions --- /** @@ -38,13 +37,13 @@ function getCardColor(suit) { * @returns {Array} The unshuffled deck of cards. */ export function createDeck() { - const deck = []; - for (const suit of SUITS) { - for (const rank of RANKS) { - deck.push({ suit, rank, faceUp: false }); - } - } - return deck; + const deck = []; + for (const suit of SUITS) { + for (const rank of RANKS) { + deck.push({ suit, rank, faceUp: false }); + } + } + return deck; } /** @@ -53,16 +52,16 @@ export function createDeck() { * @returns {Array} The shuffled array (mutated in place). */ export function shuffle(array) { - let currentIndex = array.length; - // While there remain elements to shuffle. - while (currentIndex !== 0) { - // Pick a remaining element. - const randomIndex = Math.floor(Math.random() * currentIndex); - currentIndex--; - // And swap it with the current element. - [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; - } - return array; + let currentIndex = array.length; + // While there remain elements to shuffle. + while (currentIndex !== 0) { + // Pick a remaining element. + const randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; + } + return array; } /** @@ -71,12 +70,12 @@ export function shuffle(array) { * @returns {function} A function that returns a pseudorandom number between 0 and 1. */ export function createSeededRNG(seed) { - return function() { - let t = seed += 0x6D2B79F5; - t = Math.imul(t ^ t >>> 15, t | 1); - t ^= t + Math.imul(t ^ t >>> 7, t | 61); - return ((t ^ t >>> 14) >>> 0) / 4294967296; - }; + return function () { + let t = (seed += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; } /** @@ -86,16 +85,16 @@ export function createSeededRNG(seed) { * @returns {Array} The shuffled array (mutated in place). */ export function seededShuffle(array, rng) { - let currentIndex = array.length; - // While there remain elements to shuffle. - while (currentIndex !== 0) { - // Pick a remaining element using the seeded RNG. - const randomIndex = Math.floor(rng() * currentIndex); - currentIndex--; - // And swap it with the current element. - [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; - } - return array; + let currentIndex = array.length; + // While there remain elements to shuffle. + while (currentIndex !== 0) { + // Pick a remaining element using the seeded RNG. + const randomIndex = Math.floor(rng() * currentIndex); + currentIndex--; + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]]; + } + return array; } /** @@ -104,31 +103,31 @@ export function seededShuffle(array, rng) { * @returns {Object} The initial gameState object for Klondike Solitaire. */ export function deal(deck) { - const gameState = { - tableauPiles: [[], [], [], [], [], [], []], - foundationPiles: [[], [], [], []], - stockPile: [], - wastePile: [], - }; + const gameState = { + tableauPiles: [[], [], [], [], [], [], []], + foundationPiles: [[], [], [], []], + stockPile: [], + wastePile: [], + }; - // Deal cards to the 7 tableau piles - for (let i = 0; i < 7; i++) { - for (let j = i; j < 7; j++) { - gameState.tableauPiles[j].push(deck.shift()); - } - } + // Deal cards to the 7 tableau piles + for (let i = 0; i < 7; i++) { + for (let j = i; j < 7; j++) { + gameState.tableauPiles[j].push(deck.shift()); + } + } - // Flip the top card of each tableau pile - gameState.tableauPiles.forEach(pile => { - if (pile.length > 0) { - pile[pile.length - 1].faceUp = true; - } - }); + // Flip the top card of each tableau pile + gameState.tableauPiles.forEach((pile) => { + if (pile.length > 0) { + pile[pile.length - 1].faceUp = true; + } + }); - // The rest of the deck becomes the stock - gameState.stockPile = deck; + // The rest of the deck becomes the stock + gameState.stockPile = deck; - return gameState; + return gameState; } /** @@ -138,59 +137,59 @@ export function deal(deck) { * @returns {boolean} True if the move is valid, false otherwise. */ export function isValidMove(gameState, moveData) { - const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData; + const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData; - // --- Get Source Pile and Card --- - let sourcePile; - if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex]; - else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile; - else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex]; - else return false; // Invalid source type + // --- Get Source Pile and Card --- + let sourcePile; + if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex]; + else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile; + else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex]; + else return false; // Invalid source type - const sourceCard = sourcePile?.[sourceCardIndex]; - if (!sourceCard || !sourceCard.faceUp) { - return false; // Cannot move a card that doesn't exist or is face-down - } + const sourceCard = sourcePile?.[sourceCardIndex]; + if (!sourceCard || !sourceCard.faceUp) { + return false; // Cannot move a card that doesn't exist or is face-down + } - // --- Validate Move TO a Tableau Pile --- - if (destPileType === 'tableauPiles') { - const destinationPile = gameState.tableauPiles[destPileIndex]; - const topCard = destinationPile[destinationPile.length - 1]; + // --- Validate Move TO a Tableau Pile --- + if (destPileType === "tableauPiles") { + const destinationPile = gameState.tableauPiles[destPileIndex]; + const topCard = destinationPile[destinationPile.length - 1]; - if (!topCard) { - // If the destination tableau is empty, only a King can be moved there. - return sourceCard.rank === 'K'; - } + if (!topCard) { + // If the destination tableau is empty, only a King can be moved there. + return sourceCard.rank === "K"; + } - // Card must be opposite color and one rank lower than the destination top card. - const sourceColor = getCardColor(sourceCard.suit); - const destColor = getCardColor(topCard.suit); - const sourceValue = getRankValue(sourceCard.rank); - const destValue = getRankValue(topCard.rank); - return sourceColor !== destColor && destValue - sourceValue === 1; - } + // Card must be opposite color and one rank lower than the destination top card. + const sourceColor = getCardColor(sourceCard.suit); + const destColor = getCardColor(topCard.suit); + const sourceValue = getRankValue(sourceCard.rank); + const destValue = getRankValue(topCard.rank); + return sourceColor !== destColor && destValue - sourceValue === 1; + } - // --- Validate Move TO a Foundation Pile --- - if (destPileType === 'foundationPiles') { - // You can only move one card at a time to a foundation pile. - const stackBeingMoved = sourcePile.slice(sourceCardIndex); - if (stackBeingMoved.length > 1) return false; + // --- Validate Move TO a Foundation Pile --- + if (destPileType === "foundationPiles") { + // You can only move one card at a time to a foundation pile. + const stackBeingMoved = sourcePile.slice(sourceCardIndex); + if (stackBeingMoved.length > 1) return false; - const destinationPile = gameState.foundationPiles[destPileIndex]; - const topCard = destinationPile[destinationPile.length - 1]; + const destinationPile = gameState.foundationPiles[destPileIndex]; + const topCard = destinationPile[destinationPile.length - 1]; - if (!topCard) { - // If the foundation is empty, only an Ace of any suit can be moved there. - return sourceCard.rank === 'A'; - } + if (!topCard) { + // If the foundation is empty, only an Ace of any suit can be moved there. + return sourceCard.rank === "A"; + } - // Card must be the same suit and one rank higher. - const sourceValue = getRankValue(sourceCard.rank); - const destValue = getRankValue(topCard.rank); - return sourceCard.suit === topCard.suit && sourceValue - destValue === 1; - } + // Card must be the same suit and one rank higher. + const sourceValue = getRankValue(sourceCard.rank); + const destValue = getRankValue(topCard.rank); + return sourceCard.suit === topCard.suit && sourceValue - destValue === 1; + } - return false; // Invalid destination type + return false; // Invalid destination type } /** @@ -199,41 +198,41 @@ export function isValidMove(gameState, moveData) { * @param {Object} moveData - The details of the move. */ export function moveCard(gameState, moveData) { - const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData; + const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex } = moveData; - let sourcePile; - if (sourcePileType === 'tableauPiles') sourcePile = gameState.tableauPiles[sourcePileIndex]; - else if (sourcePileType === 'wastePile') sourcePile = gameState.wastePile; - else if (sourcePileType === 'foundationPiles') sourcePile = gameState.foundationPiles[sourcePileIndex]; + let sourcePile; + if (sourcePileType === "tableauPiles") sourcePile = gameState.tableauPiles[sourcePileIndex]; + else if (sourcePileType === "wastePile") sourcePile = gameState.wastePile; + else if (sourcePileType === "foundationPiles") sourcePile = gameState.foundationPiles[sourcePileIndex]; - let destPile; - if (destPileType === 'tableauPiles') destPile = gameState.tableauPiles[destPileIndex]; - else if (destPileType === 'foundationPiles') destPile = gameState.foundationPiles[destPileIndex]; + let destPile; + if (destPileType === "tableauPiles") destPile = gameState.tableauPiles[destPileIndex]; + else if (destPileType === "foundationPiles") destPile = gameState.foundationPiles[destPileIndex]; - // Cut the entire stack of cards to be moved from the source pile. - const cardsToMove = sourcePile.splice(sourceCardIndex); - // Add the stack to the destination pile. - destPile.push(...cardsToMove); + // Cut the entire stack of cards to be moved from the source pile. + const cardsToMove = sourcePile.splice(sourceCardIndex); + // Add the stack to the destination pile. + destPile.push(...cardsToMove); - const histMove = { - move: 'move', - sourcePileType: sourcePileType, - sourcePileIndex: sourcePileIndex, - sourceCardIndex: sourceCardIndex, - destPileType: destPileType, - destPileIndex: destPileIndex, - cardsMoved: cardsToMove, - cardWasFlipped: false, - points: destPileType === 'foundationPiles' ? 11 : 1 // Points for moving to foundation - } + const histMove = { + move: "move", + sourcePileType: sourcePileType, + sourcePileIndex: sourcePileIndex, + sourceCardIndex: sourceCardIndex, + destPileType: destPileType, + destPileIndex: destPileIndex, + cardsMoved: cardsToMove, + cardWasFlipped: false, + points: destPileType === "foundationPiles" ? 11 : 1, // Points for moving to foundation + }; - // If the source was a tableau pile and there are cards left, flip the new top card. - if (sourcePileType === 'tableauPiles' && sourcePile.length > 0) { - sourcePile[sourcePile.length - 1].faceUp = true; - histMove.cardWasFlipped = true; - } + // If the source was a tableau pile and there are cards left, flip the new top card. + if (sourcePileType === "tableauPiles" && sourcePile.length > 0) { + sourcePile[sourcePile.length - 1].faceUp = true; + histMove.cardWasFlipped = true; + } - gameState.hist.push(histMove) + gameState.hist.push(histMove); } /** @@ -241,52 +240,51 @@ export function moveCard(gameState, moveData) { * @param {Object} gameState - The current state of the game. */ export function drawCard(gameState) { - if (gameState.stockPile.length > 0) { - const card = gameState.stockPile.pop(); - card.faceUp = true; - gameState.wastePile.push(card); - gameState.hist.push({ - move: 'draw', - card: card - }) - } else if (gameState.wastePile.length > 0) { - // When stock is empty, move the entire waste pile back to stock, face down. - gameState.stockPile = gameState.wastePile.reverse(); - gameState.stockPile.forEach(card => (card.faceUp = false)); - gameState.wastePile = []; - gameState.hist.push({ - move: 'draw-reset', - }) - } + if (gameState.stockPile.length > 0) { + const card = gameState.stockPile.pop(); + card.faceUp = true; + gameState.wastePile.push(card); + gameState.hist.push({ + move: "draw", + card: card, + }); + } else if (gameState.wastePile.length > 0) { + // When stock is empty, move the entire waste pile back to stock, face down. + gameState.stockPile = gameState.wastePile.reverse(); + gameState.stockPile.forEach((card) => (card.faceUp = false)); + gameState.wastePile = []; + gameState.hist.push({ + move: "draw-reset", + }); + } } export function draw3Cards(gameState) { - if (gameState.stockPile.length > 0) { - let cards = [] - for (let i = 0; i < 3; i++) { - if (gameState.stockPile.length > 0) { - const card = gameState.stockPile.pop(); - card.faceUp = true; - gameState.wastePile.push(card); - cards.push(card); - } else { - break; // Stop if stock runs out - } - } - gameState.hist.push({ - move: 'draw-3', - cards: cards, - }) - } else if (gameState.wastePile.length > 0) { - // When stock is empty, move the entire waste pile back to stock, face down. - gameState.stockPile = gameState.wastePile.reverse(); - gameState.stockPile.forEach(card => (card.faceUp = false)); - gameState.wastePile = []; - gameState.hist.push({ - move: 'draw-reset', - }) - } - + if (gameState.stockPile.length > 0) { + let cards = []; + for (let i = 0; i < 3; i++) { + if (gameState.stockPile.length > 0) { + const card = gameState.stockPile.pop(); + card.faceUp = true; + gameState.wastePile.push(card); + cards.push(card); + } else { + break; // Stop if stock runs out + } + } + gameState.hist.push({ + move: "draw-3", + cards: cards, + }); + } else if (gameState.wastePile.length > 0) { + // When stock is empty, move the entire waste pile back to stock, face down. + gameState.stockPile = gameState.wastePile.reverse(); + gameState.stockPile.forEach((card) => (card.faceUp = false)); + gameState.wastePile = []; + gameState.hist.push({ + move: "draw-reset", + }); + } } /** @@ -295,8 +293,8 @@ export function draw3Cards(gameState) { * @returns {boolean} True if the game is won. */ export function checkWinCondition(gameState) { - const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0); - return foundationCardCount === 52; + const foundationCardCount = gameState.foundationPiles.reduce((acc, pile) => acc + pile.length, 0); + return foundationCardCount === 52; } /** @@ -305,64 +303,64 @@ export function checkWinCondition(gameState) { * @returns {boolean} True if the game can be auto-solved. */ export function checkAutoSolve(gameState) { - if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false; - for (const pile of gameState.tableauPiles) { - for (const card of pile) { - if (!card.faceUp) return false; - } - } - return true; + if (gameState.stockPile.length > 0 || gameState.wastePile.length > 0) return false; + for (const pile of gameState.tableauPiles) { + for (const card of pile) { + if (!card.faceUp) return false; + } + } + return true; } export function autoSolveMoves(userId, gameState) { - const moves = []; - const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles)); - const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles)); + const moves = []; + const foundations = JSON.parse(JSON.stringify(gameState.foundationPiles)); + const tableau = JSON.parse(JSON.stringify(gameState.tableauPiles)); - function canMoveToFoundation(card) { - let foundationPile = foundations.find(pile => pile[pile.length - 1]?.suit === card.suit); - if (!foundationPile) { - foundationPile = foundations.find(pile => pile.length === 0); - } - if (foundationPile.length === 0) { - return card.rank === 'A'; // Only Ace can be placed on empty foundation - } else { - const topCard = foundationPile[foundationPile.length - 1]; - return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1; - } - } + function canMoveToFoundation(card) { + let foundationPile = foundations.find((pile) => pile[pile.length - 1]?.suit === card.suit); + if (!foundationPile) { + foundationPile = foundations.find((pile) => pile.length === 0); + } + if (foundationPile.length === 0) { + return card.rank === "A"; // Only Ace can be placed on empty foundation + } else { + const topCard = foundationPile[foundationPile.length - 1]; + return card.suit === topCard.suit && getRankValue(card.rank) === getRankValue(topCard.rank) + 1; + } + } - let moved; - do { - moved = false; + let moved; + do { + moved = false; - for (let i = 0; i < tableau.length; i++) { - const column = tableau[i]; - if (column.length === 0) continue; + for (let i = 0; i < tableau.length; i++) { + const column = tableau[i]; + if (column.length === 0) continue; - const card = column[column.length - 1]; // Top card of the tableau column - let foundationIndex = foundations.findIndex(pile => pile[pile.length - 1]?.suit === card.suit); - if (foundationIndex === -1) { - foundationIndex = foundations.findIndex(pile => pile.length === 0); - } - if(canMoveToFoundation(card)) { - let moveData = { - destPileIndex: foundationIndex, - destPileType: 'foundationPiles', - sourceCardIndex: column.length - 1, - sourcePileIndex: i, - sourcePileType: 'tableauPiles', - userId: userId, - } - tableau[i].pop() - foundations[foundationIndex].push(card) - //moveCard(gameState, moveData) - moves.push(moveData); - moved = true; - } - } - } while (moved)//(foundations.reduce((acc, pile) => acc + pile.length, 0)); - emitSolitaireUpdate(userId, moves) + const card = column[column.length - 1]; // Top card of the tableau column + let foundationIndex = foundations.findIndex((pile) => pile[pile.length - 1]?.suit === card.suit); + if (foundationIndex === -1) { + foundationIndex = foundations.findIndex((pile) => pile.length === 0); + } + if (canMoveToFoundation(card)) { + let moveData = { + destPileIndex: foundationIndex, + destPileType: "foundationPiles", + sourceCardIndex: column.length - 1, + sourcePileIndex: i, + sourcePileType: "tableauPiles", + userId: userId, + }; + tableau[i].pop(); + foundations[foundationIndex].push(card); + //moveCard(gameState, moveData) + moves.push(moveData); + moved = true; + } + } + } while (moved); //(foundations.reduce((acc, pile) => acc + pile.length, 0)); + emitSolitaireUpdate(userId, moves); } /** @@ -371,98 +369,99 @@ export function autoSolveMoves(userId, gameState) { * @param {Object} gameState - The current game state, which includes a `hist` array. */ export function undoMove(gameState) { - if (!gameState.hist || gameState.hist.length === 0) { - console.log("No moves to undo."); - return; // Nothing to undo - } + if (!gameState.hist || gameState.hist.length === 0) { + console.log("No moves to undo."); + return; // Nothing to undo + } - const lastMove = gameState.hist.pop(); // Get and remove the last move from history - gameState.moves++; // Undoing a move counts as a new move - gameState.score -= lastMove.points || 1; // Revert score based on points from the last move + const lastMove = gameState.hist.pop(); // Get and remove the last move from history + gameState.moves++; // Undoing a move counts as a new move + gameState.score -= lastMove.points || 1; // Revert score based on points from the last move - switch (lastMove.move) { - case 'move': - undoCardMove(gameState, lastMove); - break; - case 'draw': - undoDraw(gameState, lastMove); - break; - case 'draw-3': - undoDraw3(gameState, lastMove); - break; - case 'draw-reset': - undoDrawReset(gameState, lastMove); - break; - default: - // If an unknown move type is found, push it back to avoid corrupting the history - gameState.hist.push(lastMove); - gameState.moves--; // Revert the move count increment - gameState.score += lastMove.points || 1; // Revert the score decrement - console.error("Unknown move type in history:", lastMove); - break; - } + switch (lastMove.move) { + case "move": + undoCardMove(gameState, lastMove); + break; + case "draw": + undoDraw(gameState, lastMove); + break; + case "draw-3": + undoDraw3(gameState, lastMove); + break; + case "draw-reset": + undoDrawReset(gameState, lastMove); + break; + default: + // If an unknown move type is found, push it back to avoid corrupting the history + gameState.hist.push(lastMove); + gameState.moves--; // Revert the move count increment + gameState.score += lastMove.points || 1; // Revert the score decrement + console.error("Unknown move type in history:", lastMove); + break; + } } // --- Helper functions for undoing specific moves --- function undoCardMove(gameState, moveData) { - const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } = moveData; + const { sourcePileType, sourcePileIndex, sourceCardIndex, destPileType, destPileIndex, cardsMoved, cardWasFlipped } = + moveData; - // 1. Find the destination pile (where the cards are NOW) - let currentPile; - if (destPileType === 'tableauPiles') currentPile = gameState.tableauPiles[destPileIndex]; - else if (destPileType === 'foundationPiles') currentPile = gameState.foundationPiles[destPileIndex]; + // 1. Find the destination pile (where the cards are NOW) + let currentPile; + if (destPileType === "tableauPiles") currentPile = gameState.tableauPiles[destPileIndex]; + else if (destPileType === "foundationPiles") currentPile = gameState.foundationPiles[destPileIndex]; - // 2. Remove the moved cards from their current pile - // Using splice with a negative index removes from the end of the array - currentPile.splice(-cardsMoved.length); + // 2. Remove the moved cards from their current pile + // Using splice with a negative index removes from the end of the array + currentPile.splice(-cardsMoved.length); - // 3. Find the original source pile - let originalPile; - if (sourcePileType === 'tableauPiles') originalPile = gameState.tableauPiles[sourcePileIndex]; - else if (sourcePileType === 'wastePile') originalPile = gameState.wastePile; - else if (sourcePileType === 'foundationPiles') originalPile = gameState.foundationPiles[sourcePileIndex]; + // 3. Find the original source pile + let originalPile; + if (sourcePileType === "tableauPiles") originalPile = gameState.tableauPiles[sourcePileIndex]; + else if (sourcePileType === "wastePile") originalPile = gameState.wastePile; + else if (sourcePileType === "foundationPiles") originalPile = gameState.foundationPiles[sourcePileIndex]; - // 4. Put the cards back where they came from - // Using splice to insert the cards back at their original index - originalPile.splice(sourceCardIndex, 0, ...cardsMoved); + // 4. Put the cards back where they came from + // Using splice to insert the cards back at their original index + originalPile.splice(sourceCardIndex, 0, ...cardsMoved); - // 5. If a card was flipped during the move, flip it back to face-down - if (cardWasFlipped) { - const cardToUnflip = originalPile[sourceCardIndex - 1]; - if (cardToUnflip) { - cardToUnflip.faceUp = false; - } - } + // 5. If a card was flipped during the move, flip it back to face-down + if (cardWasFlipped) { + const cardToUnflip = originalPile[sourceCardIndex - 1]; + if (cardToUnflip) { + cardToUnflip.faceUp = false; + } + } } function undoDraw(gameState, moveData) { - // A 'draw' move means a card went from stock to waste. - // To undo, move it from waste back to stock and flip it face-down. - const cardToReturn = gameState.wastePile.pop(); - if (cardToReturn) { - cardToReturn.faceUp = false; - gameState.stockPile.push(cardToReturn); - } + // A 'draw' move means a card went from stock to waste. + // To undo, move it from waste back to stock and flip it face-down. + const cardToReturn = gameState.wastePile.pop(); + if (cardToReturn) { + cardToReturn.faceUp = false; + gameState.stockPile.push(cardToReturn); + } } function undoDraw3(gameState, moveData) { - // A 'draw-3' move means up to 3 cards went from stock to - // waste. To undo, move them back to stock and flip them face-down. - const cardsToReturn = moveData.cards || []; - for (let i = 0; i < cardsToReturn.length; i++) { - const card = gameState.wastePile.pop(); - if (card) { - card.faceUp = false; - gameState.stockPile.push(card); - } - } + // A 'draw-3' move means up to 3 cards went from stock to + // waste. To undo, move them back to stock and flip them face-down. + const cardsToReturn = moveData.cards || []; + for (let i = 0; i < cardsToReturn.length; i++) { + const card = gameState.wastePile.pop(); + if (card) { + card.faceUp = false; + gameState.stockPile.push(card); + } + } } function undoDrawReset(gameState, moveData) { - // A 'draw-reset' means the waste pile was moved to the stock pile. - // To undo, move the stock pile back to the waste pile and flip cards face-up. - gameState.wastePile = gameState.stockPile.reverse(); - gameState.wastePile.forEach(card => (card.faceUp = true)); - gameState.stockPile = []; -} \ No newline at end of file + // A 'draw-reset' means the waste pile was moved to the stock pile. + // To undo, move the stock pile back to the waste pile and flip cards face-up. + gameState.wastePile = gameState.stockPile.reverse(); + gameState.wastePile.forEach((card) => (card.faceUp = true)); + gameState.stockPile = []; +} diff --git a/src/game/state.js b/src/game/state.js index b71b53b..0c503cd 100644 --- a/src/game/state.js +++ b/src/game/state.js @@ -44,7 +44,6 @@ export let activePredis = {}; // Format: { [userId]: { endAt, lastMessage } } export let activeSlowmodes = {}; - // --- Queues for Matchmaking --- // Stores user IDs waiting to play Tic-Tac-Toe. @@ -55,7 +54,6 @@ export let connect4Queue = []; export let queueMessagesEndpoints = []; - // --- Rate Limiting and Caching --- // Tracks message timestamps for the channel points system, keyed by user ID. @@ -70,4 +68,4 @@ export let requestTimestamps = new Map(); // In-memory cache for Valorant skin data fetched from the API. // This prevents re-fetching the same data on every command use. -export let skins = []; \ No newline at end of file +export let skins = []; diff --git a/src/game/various.js b/src/game/various.js index c7d4e6a..f6844d8 100644 --- a/src/game/various.js +++ b/src/game/various.js @@ -4,19 +4,19 @@ export const C4_COLS = 7; // A predefined list of choices for the /timeout command's duration option. const TimesChoices = [ - { name: '1 minute', value: 60 }, - { name: '5 minutes', value: 300 }, - { name: '10 minutes', value: 600 }, - { name: '15 minutes', value: 900 }, - { name: '30 minutes', value: 1800 }, - { name: '1 heure', value: 3600 }, - { name: '2 heures', value: 7200 }, - { name: '3 heures', value: 10800 }, - { name: '6 heures', value: 21600 }, - { name: '9 heures', value: 32400 }, - { name: '12 heures', value: 43200 }, - { name: '16 heures', value: 57600 }, - { name: '1 jour', value: 86400 }, + { name: "1 minute", value: 60 }, + { name: "5 minutes", value: 300 }, + { name: "10 minutes", value: 600 }, + { name: "15 minutes", value: 900 }, + { name: "30 minutes", value: 1800 }, + { name: "1 heure", value: 3600 }, + { name: "2 heures", value: 7200 }, + { name: "3 heures", value: 10800 }, + { name: "6 heures", value: 21600 }, + { name: "9 heures", value: 32400 }, + { name: "12 heures", value: 43200 }, + { name: "16 heures", value: 57600 }, + { name: "1 jour", value: 86400 }, ]; /** @@ -24,10 +24,9 @@ const TimesChoices = [ * @returns {Array} The array of time choices. */ export function getTimesChoices() { - return TimesChoices; + return TimesChoices; } - // --- Connect 4 Logic --- /** @@ -35,7 +34,9 @@ export function getTimesChoices() { * @returns {Array>} A 2D array representing the board. */ export function createConnect4Board() { - return Array(C4_ROWS).fill(null).map(() => Array(C4_COLS).fill(null)); + return Array(C4_ROWS) + .fill(null) + .map(() => Array(C4_COLS).fill(null)); } /** @@ -45,43 +46,95 @@ export function createConnect4Board() { * @returns {object} An object with `win` (boolean) and `pieces` (array of winning piece coordinates). */ export function checkConnect4Win(board, player) { - // Check horizontal - for (let r = 0; r < C4_ROWS; r++) { - for (let c = 0; c <= C4_COLS - 4; c++) { - if (board[r][c] === player && board[r][c+1] === player && board[r][c+2] === player && board[r][c+3] === player) { - return { win: true, pieces: [{row:r, col:c}, {row:r, col:c+1}, {row:r, col:c+2}, {row:r, col:c+3}] }; - } - } - } + // Check horizontal + for (let r = 0; r < C4_ROWS; r++) { + for (let c = 0; c <= C4_COLS - 4; c++) { + if ( + board[r][c] === player && + board[r][c + 1] === player && + board[r][c + 2] === player && + board[r][c + 3] === player + ) { + return { + win: true, + pieces: [ + { row: r, col: c }, + { row: r, col: c + 1 }, + { row: r, col: c + 2 }, + { row: r, col: c + 3 }, + ], + }; + } + } + } - // Check vertical - for (let r = 0; r <= C4_ROWS - 4; r++) { - for (let c = 0; c < C4_COLS; c++) { - if (board[r][c] === player && board[r+1][c] === player && board[r+2][c] === player && board[r+3][c] === player) { - return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c}, {row:r+2, col:c}, {row:r+3, col:c}] }; - } - } - } + // Check vertical + for (let r = 0; r <= C4_ROWS - 4; r++) { + for (let c = 0; c < C4_COLS; c++) { + if ( + board[r][c] === player && + board[r + 1][c] === player && + board[r + 2][c] === player && + board[r + 3][c] === player + ) { + return { + win: true, + pieces: [ + { row: r, col: c }, + { row: r + 1, col: c }, + { row: r + 2, col: c }, + { row: r + 3, col: c }, + ], + }; + } + } + } - // Check diagonal (down-right) - for (let r = 0; r <= C4_ROWS - 4; r++) { - for (let c = 0; c <= C4_COLS - 4; c++) { - if (board[r][c] === player && board[r+1][c+1] === player && board[r+2][c+2] === player && board[r+3][c+3] === player) { - return { win: true, pieces: [{row:r, col:c}, {row:r+1, col:c+1}, {row:r+2, col:c+2}, {row:r+3, col:c+3}] }; - } - } - } + // Check diagonal (down-right) + for (let r = 0; r <= C4_ROWS - 4; r++) { + for (let c = 0; c <= C4_COLS - 4; c++) { + if ( + board[r][c] === player && + board[r + 1][c + 1] === player && + board[r + 2][c + 2] === player && + board[r + 3][c + 3] === player + ) { + return { + win: true, + pieces: [ + { row: r, col: c }, + { row: r + 1, col: c + 1 }, + { row: r + 2, col: c + 2 }, + { row: r + 3, col: c + 3 }, + ], + }; + } + } + } - // Check diagonal (up-right) - for (let r = 3; r < C4_ROWS; r++) { - for (let c = 0; c <= C4_COLS - 4; c++) { - if (board[r][c] === player && board[r-1][c+1] === player && board[r-2][c+2] === player && board[r-3][c+3] === player) { - return { win: true, pieces: [{row:r, col:c}, {row:r-1, col:c+1}, {row:r-2, col:c+2}, {row:r-3, col:c+3}] }; - } - } - } + // Check diagonal (up-right) + for (let r = 3; r < C4_ROWS; r++) { + for (let c = 0; c <= C4_COLS - 4; c++) { + if ( + board[r][c] === player && + board[r - 1][c + 1] === player && + board[r - 2][c + 2] === player && + board[r - 3][c + 3] === player + ) { + return { + win: true, + pieces: [ + { row: r, col: c }, + { row: r - 1, col: c + 1 }, + { row: r - 2, col: c + 2 }, + { row: r - 3, col: c + 3 }, + ], + }; + } + } + } - return { win: false, pieces: [] }; + return { win: false, pieces: [] }; } /** @@ -90,8 +143,8 @@ export function checkConnect4Win(board, player) { * @returns {boolean} True if the game is a draw. */ export function checkConnect4Draw(board) { - // A draw occurs if the top row is completely full. - return board[0].every(cell => cell !== null); + // A draw occurs if the top row is completely full. + return board[0].every((cell) => cell !== null); } /** @@ -100,10 +153,10 @@ export function checkConnect4Draw(board) { * @returns {string} The formatted string representation of the board. */ export function formatConnect4BoardForDiscord(board) { - const symbols = { - 'R': '🔴', - 'Y': '🟡', - null: '⚪' // Using a white circle for empty slots - }; - return board.map(row => row.map(cell => symbols[cell]).join('')).join('\n'); -} \ No newline at end of file + const symbols = { + R: "🔴", + Y: "🟡", + null: "⚪", // Using a white circle for empty slots + }; + return board.map((row) => row.map((cell) => symbols[cell]).join("")).join("\n"); +} diff --git a/src/server/app.js b/src/server/app.js index 897b277..dcb7484 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -1,60 +1,65 @@ -import 'dotenv/config'; -import express from 'express'; -import { verifyKeyMiddleware } from 'discord-interactions'; -import { handleInteraction } from '../bot/handlers/interactionCreate.js'; -import { client } from '../bot/client.js'; +import "dotenv/config"; +import express from "express"; +import { verifyKeyMiddleware } from "discord-interactions"; +import { handleInteraction } from "../bot/handlers/interactionCreate.js"; +import { client } from "../bot/client.js"; // Import route handlers -import { apiRoutes } from './routes/api.js'; -import { pokerRoutes } from './routes/poker.js'; -import { solitaireRoutes } from './routes/solitaire.js'; -import {getSocketIo} from "./socket.js"; -import {erinyesRoutes} from "./routes/erinyes.js"; -import {blackjackRoutes} from "./routes/blackjack.js"; +import { apiRoutes } from "./routes/api.js"; +import { pokerRoutes } from "./routes/poker.js"; +import { solitaireRoutes } from "./routes/solitaire.js"; +import { getSocketIo } from "./socket.js"; +import { blackjackRoutes } from "./routes/blackjack.js"; +import { marketRoutes } from "./routes/market.js"; // --- EXPRESS APP INITIALIZATION --- const app = express(); const io = getSocketIo(); -const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; +const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL; // --- GLOBAL MIDDLEWARE --- // CORS Middleware app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', FLAPI_URL); - res.header('Access-Control-Allow-Headers', 'Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires'); - next(); + res.header("Access-Control-Allow-Origin", FLAPI_URL); + res.header( + "Access-Control-Allow-Headers", + "Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires", + ); + next(); }); // --- PRIMARY DISCORD INTERACTION ENDPOINT --- // This endpoint handles all interactions sent from Discord (slash commands, buttons, etc.) -app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => { - // The actual logic is delegated to a dedicated handler for better organization - await handleInteraction(req, res, client); +app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (req, res) => { + // The actual logic is delegated to a dedicated handler for better organization + await handleInteraction(req, res, client); }); // JSON Body Parser Middleware app.use(express.json()); // --- STATIC ASSETS --- -app.use('/public', express.static('public')); - +app.use("/public", express.static("public")); // --- API ROUTES --- // General API routes (users, polls, etc.) -app.use('/api', apiRoutes(client, io)); +app.use("/api", apiRoutes(client, io)); // Poker-specific routes -app.use('/api/poker', pokerRoutes(client, io)); +app.use("/api/poker", pokerRoutes(client, io)); // Solitaire-specific routes -app.use('/api/solitaire', solitaireRoutes(client, io)); +app.use("/api/solitaire", solitaireRoutes(client, io)); -app.use('/api/blackjack', blackjackRoutes(client, io)); +// Blackjack-specific routes +app.use("/api/blackjack", blackjackRoutes(client, io)); + +// Market-specific routes +app.use("/api/market-place", marketRoutes(client, io)); // erinyes-specific routes -app.use('/api/erinyes', erinyesRoutes(client, io)); +// app.use("/api/erinyes", erinyesRoutes(client, io)); - -export { app }; \ No newline at end of file +export { app }; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index df13bf7..f18b33d 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -1,24 +1,34 @@ -import express from 'express'; -import { sleep } from 'openai/core'; +import express from "express"; +import { sleep } from "openai/core"; // --- Database Imports --- import { - getAllUsers, getUsersByElo, pruneOldLogs, getLogs, getUser, - getUserLogs, getUserElo, getUserGames, getUserInventory, - queryDailyReward, updateUserCoins, insertLog, getAllAkhys, insertUser, insertElos -} from '../../database/index.js'; + getAllAkhys, + getAllUsers, + getLogs, + getUser, + getUserElo, + getUserGames, + getUserInventory, + getUserLogs, + getUsersByElo, + insertLog, + insertUser, + pruneOldLogs, + queryDailyReward, + updateUserCoins, +} from "../../database/index.js"; // --- Game State Imports --- -import {activePolls, activeSlowmodes, activePredis, skins} from '../../game/state.js'; +import { activePolls, activePredis, activeSlowmodes, skins } from "../../game/state.js"; // --- Utility and API Imports --- -import { getOnlineUsersWithRole } from '../../utils/index.js'; -import { DiscordRequest } from '../../api/discord.js'; +import { formatTime } from "../../utils/index.js"; +import { DiscordRequest } from "../../api/discord.js"; // --- Discord.js Builder Imports --- -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import {emitDataUpdated, socketEmit} from "../socket.js"; -import { formatTime } from "../../utils/index.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { emitDataUpdated, socketEmit } from "../socket.js"; // Create a new router instance const router = express.Router(); @@ -30,859 +40,888 @@ const router = express.Router(); * @returns {object} The configured Express router. */ export function apiRoutes(client, io) { - // --- Server Health & Basic Data --- - - router.get('/check', (req, res) => { - res.status(200).json({ status: 'OK', message: 'FlopoBot API is running.' }); - }); - - router.get('/users', (req, res) => { - try { - const users = getAllUsers.all(); - res.json(users); - } catch (error) { - console.error("Error fetching users:", error); - res.status(500).json({ error: 'Failed to fetch users.' }); - } - }); - - router.get('/akhys', (req, res) => { - try { - const akhys = getAllAkhys.all() - res.json(akhys); - } catch (error) { - console.error("Error fetching akhys:", error); - res.status(500).json({ error: 'Failed to fetch akhys' }); - } - }) - - router.post('/register-user', async (req, res) => { - const { discordUserId } = req.body; - const discordUser = await client.users.fetch(discordUserId); - - try { - insertUser.run({ - id: discordUser.id, - username: discordUser.username, - globalName: discordUser.globalName, - warned: 0, - warns: 0, - allTimeWarns: 0, - totalRequests: 0, - avatarUrl: discordUser.displayAvatarURL({ dynamic: true, size: 256 }), - isAkhy: 0 - }) - - updateUserCoins.run({ id: discordUser.id, coins: 5000 }); - insertLog.run({ - id: `${discordUser.id}-welcome-${Date.now()}`, - user_id: discordUser.id, - action: 'WELCOME_BONUS', - target_user_id: null, - coins_amount: 5000, - user_new_amount: 5000, - }) - - console.log(`New registered user: ${discordUser.username} (${discordUser.id})`); - - res.status(200).json({ message: `Bienvenue ${discordUser.username} !` }); - } catch (e) { - console.log(`Failed to register user ${discordUser.username} (${discordUser.id})`, e); - res.status(500).json({ error: 'Erreur lors de la création du nouvel utilisateur.' }); - } - }) - - router.get('/skins', (req, res) => { - try { - res.json(skins) - } catch (error) { - console.error("Error fetching skins:", error); - res.status(500).json({ error: 'Failed to fetch skins.' }); - } - }) - - router.get('/skin/:id', (req, res) => { - try { - const skinData = skins.find((s) => s.uuid === req.params.id); - res.json(skinData) - } catch (error) { - console.error("Error fetching skin:", error); - res.status(500).json({ error: 'Failed to fetch skin.' }); - } - }) - - router.post('/skin/:id', (req, res) => { - const { level, chroma } = req.body; - try { - const skinData = skins.find((s) => s.uuid === req.params.id); - if (!skinData) res.status(404).json({ error: 'Invalid skin.' }); - - const levelData = skinData.levels[level - 1] || {}; - const chromaData = skinData.chromas[chroma - 1] || {}; - - let videoUrl = null; - if (level === skinData.levels.length) { - videoUrl = chromaData.streamedVideo; - } - videoUrl = videoUrl || levelData.streamedVideo; - - res.json({ url: videoUrl }); - } catch (error) { - console.error("Error fetching skins:", error); - res.status(500).json({ error: 'Failed to fetch skins.' }); - } - }) - - router.get('/users/by-elo', (req, res) => { - try { - const users = getUsersByElo.all(); - res.json(users); - } catch (error) { - console.error("Error fetching users by Elo:", error); - res.status(500).json({ error: 'Failed to fetch users by Elo.' }); - } - }); - - router.get('/logs', async (req, res) => { - try { - await pruneOldLogs(); - const logs = getLogs.all(); - res.status(200).json(logs); - } catch (error) { - console.error("Error fetching logs:", error); - res.status(500).json({ error: 'Failed to fetch logs.' }); - } - }); - - // --- User-Specific Routes --- - - router.get('/user/:id/avatar', async (req, res) => { - try { - const user = await client.users.fetch(req.params.id); - const avatarUrl = user.displayAvatarURL({ format: 'png', size: 256 }); - res.json({ avatarUrl }); - } catch (error) { - res.status(404).json({ error: 'User not found or failed to fetch avatar.' }); - } - }); - - router.get('/user/:id/username', async (req, res) => { - try { - const user = await client.users.fetch(req.params.id); - res.json({ user }); - } catch (error) { - res.status(404).json({ error: 'User not found.' }); - } - }); - - router.get('/user/:id/sparkline', (req, res) => { - try { - const logs = getUserLogs.all({ user_id: req.params.id }); - res.json({ sparkline: logs }); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch logs for sparkline.' }); - } - }); - - router.get('/user/:id/elo', (req, res) => { - try { - const eloData = getUserElo.get({ id: req.params.id }); - res.json({ elo: eloData?.elo || null }); - } catch(e) { - res.status(500).json({ error: 'Failed to fetch Elo data.' }); - } - }); - - router.get('/user/:id/elo-graph', (req, res) => { - try { - const games = getUserGames.all({ user_id: req.params.id }); - const eloHistory = games.map(game => game.p1 === req.params.id ? game.p1_new_elo : game.p2_new_elo); - res.json({ elo_graph: eloHistory }); - } catch (e) { - res.status(500).json({ error: 'Failed to generate Elo graph.' }); - } - }); - - router.get('/user/:id/inventory', (req, res) => { - try { - const inventory = getUserInventory.all({ user_id: req.params.id }); - res.json({ inventory }); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch inventory.' }); - } - }); - - router.get('/user/:id/games-history', async (req, res) => { - try { - const games = getUserGames.all({ user_id: req.params.id }); - res.json({ games }) - } catch (err) { - res.status(500).json({ error: 'Failed to fetch games history.' }); - } - }) - - router.get('/user/:id/daily', async (req, res) => { - const { id } = req.params; - try { - const akhy = getUser.get(id); - if (!akhy) return res.status(404).json({ message: 'Utilisateur introuvable' }); - if (akhy.dailyQueried) return res.status(403).json({ message: 'Récompense journalière déjà récupérée.' }); - - const amount = 500; - const newCoins = akhy.coins + amount; - queryDailyReward.run(id); - updateUserCoins.run({ id, coins: newCoins }); - insertLog.run({ - id: `${id}-daily-${Date.now()}`, user_id: id, action: 'DAILY_REWARD', - target_user_id: null, - coins_amount: amount, user_new_amount: newCoins, - }); - - await socketEmit('daily-queried', { userId: id }); - res.status(200).json({ message: `+${amount} FlopoCoins! Récompense récupérée !` }); - } catch (error) { - console.log() - res.status(500).json({ error: "Failed to process daily reward." }); - } - }); - - // --- Poll & Timeout Routes --- - - router.get('/polls', (req, res) => { - res.json({ activePolls }); - }); - - router.post('/timedout', async (req, res) => { - try { - const { userId } = req.body; - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const member = await guild.members.fetch(userId); - res.status(200).json({ isTimedOut: member?.isCommunicationDisabled() || false }); - } catch (e) { - res.status(404).send({ message: 'Member not found or guild unavailable.' }); - } - }); - - // --- Shop & Interaction Routes --- - - router.post('/change-nickname', async (req, res) => { - const { userId, nickname, commandUserId } = req.body; - const commandUser = getUser.get(commandUserId); - if (!commandUser) return res.status(404).json({ message: 'Command user not found.' }); - if (commandUser.coins < 1000) return res.status(403).json({ message: 'Pas assez de FlopoCoins (1000 requis).' }); - - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const member = await guild.members.fetch(userId); - const old_nickname = member.nickname; - await member.setNickname(nickname); - - const newCoins = commandUser.coins - 1000; - updateUserCoins.run({ id: commandUserId, coins: newCoins }); - insertLog.run({ - id: `${commandUserId}-changenick-${Date.now()}`, - user_id: commandUserId, - action: 'CHANGE_NICKNAME', - target_user_id: userId, - coins_amount: -1000, - user_new_amount: newCoins, - }); - - console.log(`${commandUserId} change nickname of ${userId}: ${old_nickname} -> ${nickname}`) - - try { - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${commandUserId}> a modifié le pseudo de <@${userId}>`) - .addFields( - { name: `${old_nickname}`, value: ``, inline: true }, - { name: `➡️`, value: ``, inline: true }, - { name: `${nickname}`, value: ``, inline: true } - ) - .setColor('#5865f2') - .setTimestamp(new Date()); - - await generalChannel.send({ embeds: [embed] }); - } catch (e) { - console.log(e) - } - - res.status(200).json({ message: `Le pseudo de ${member.user.username} a été changé.` }); - } catch (error) { - res.status(500).json({ message: `Erreur: Impossible de changer le pseudo.` }); - } - }); - - router.post('/spam-ping', async (req, res) => { - const { userId, commandUserId } = req.body; - - const user = getUser.get(userId); - const commandUser = getUser.get(commandUserId); - - if (!commandUser || !user) return res.status(404).json({ message: 'Oups petit soucis' }); - - if (commandUser.coins < 5000) return res.status(403).json({ message: 'Pas assez de coins' }); - - try { - const discordUser = await client.users.fetch(userId); - - await discordUser.send(`<@${userId}>`) - - res.status(200).json({ message : 'C\'est parti ehehe' }); - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 5000, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'SPAM_PING', - target_user_id: userId, - coins_amount: -5000, - user_new_amount: commandUser.coins - 5000, - }) - await emitDataUpdated({ table: 'users', action: 'update' }); - - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${commandUserId}> a envoyé un spam ping à <@${userId}>`) - .setColor('#5865f2') - .setTimestamp(new Date()); - - await generalChannel.send({ embeds: [embed] }); - } catch (e) { - console.log(e) - } - - for (let i = 1; i < 120; i++) { - await discordUser.send(`<@${userId}>`) - await sleep(250); - } - - - } catch (err) { - console.log(err) - res.status(500).json({ message : "Oups ça n'a pas marché" }); - } - }); - - // --- Slowmode Routes --- - - router.get('/slowmodes', (req, res) => { - res.status(200).json({ slowmodes: activeSlowmodes }); - }); - - router.post('/slowmode', async (req, res) => { - let { userId, commandUserId} = req.body - - const user = getUser.get(userId) - const commandUser = getUser.get(commandUserId); - - if (!commandUser || !user) return res.status(404).json({ message: 'Oups petit soucis' }); - - if (commandUser.coins < 10000) return res.status(403).json({ message: 'Pas assez de coins' }); - - if (!user) return res.status(403).send({ message: 'Oups petit problème'}) - - if (activeSlowmodes[userId]) { - if (userId === commandUserId) { - delete activeSlowmodes[userId]; - await socketEmit('new-slowmode', { action: 'new slowmode' }); - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 10000, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'SLOWMODE', - target_user_id: userId, - coins_amount: -10000, - user_new_amount: commandUser.coins - 10000, - }) - - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${commandUserId}> a retiré son slowmode`) - .setColor('#5865f2') - .setTimestamp(new Date()); - - await generalChannel.send({ embeds: [embed] }); - } catch (e) { - console.log(e) - } - return res.status(200).json({ message: 'Slowmode retiré'}) - } else { - let timeLeft = (activeSlowmodes[userId].endAt - Date.now())/1000 - timeLeft = timeLeft > 60 ? (timeLeft/60).toFixed()?.toString() + 'min' : timeLeft.toFixed()?.toString() + 'sec' - return res.status(403).json({ message: `${user.globalName} est déjà en slowmode (${timeLeft})`}) - } - } else if (userId === commandUserId) { - return res.status(403).json({ message: 'Impossible de te mettre toi-même en slowmode'}) - } - - activeSlowmodes[userId] = { - userId: userId, - endAt: Date.now() + 60 * 60 * 1000, // 1 heure - lastMessage: null, - }; - await socketEmit('new-slowmode', { action: 'new slowmode' }); - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 10000, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'SLOWMODE', - target_user_id: userId, - coins_amount: -10000, - user_new_amount: commandUser.coins - 10000, - }) - await emitDataUpdated({ table: 'users', action: 'update' }); - - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${commandUserId}> a mis <@${userId}> en slowmode pendant 1h`) - .setColor('#5865f2') - .setTimestamp(new Date()); - - await generalChannel.send({ embeds: [embed] }); - } catch (e) { - console.log(e) - } - - return res.status(200).json({ message: `${user.globalName} est maintenant en slowmode pour 1h`}) - }); - - // --- Time-Out Route --- - - router.post('/timeout', async (req, res) => { - let { userId, commandUserId} = req.body - - const user = getUser.get(userId) - const commandUser = getUser.get(commandUserId); - - if (!commandUser || !user) return res.status(404).json({ message: 'Oups petit soucis' }); - - if (commandUser.coins < 100000) return res.status(403).json({ message: 'Pas assez de coins' }); - - if (!user) return res.status(403).send({ message: 'Oups petit problème'}) - - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const member = await guild.members.fetch(userId); - - if (userId === commandUserId) { - if (member && - (!member.communicationDisabledUntilTimestamp || - member.communicationDisabledUntilTimestamp < Date.now())) { - return res.status(403).json({ message: `Impossible de t'auto time-out`}) - } - await socketEmit('new-timeout', { action: 'new slowmode' }); - - try { - const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${userId}`; - await DiscordRequest(endpointTimeout, { - method: 'PATCH', - body: { communication_disabled_until: new Date(Date.now()).toISOString() }, - }); - } catch (e) { - console.log(e) - return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` }); - } - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 10000, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'TIMEOUT', - target_user_id: userId, - coins_amount: -10000, - user_new_amount: commandUser.coins - 10000, - }) - - try { - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${commandUserId}> a retiré son time-out`) - .setColor('#5865f2') - .setTimestamp(new Date()); - - await generalChannel.send({ embeds: [embed] }); - } catch (e) { - console.log(e) - } - return res.status(200).json({ message: 'Time-out retiré'}) - } - - if (member && - member.communicationDisabledUntilTimestamp && - member.communicationDisabledUntilTimestamp > Date.now()) { - return res.status(403).json({ message: `${user.globalName} est déjà time-out`}) - } - - try { - const timeoutUntil = new Date(Date.now() + 43200 * 1000).toISOString(); - const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${userId}`; - await DiscordRequest(endpointTimeout, { - method: 'PATCH', - body: { communication_disabled_until: timeoutUntil }, - }); - } catch (e) { - console.log(e) - return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` }); - } - - await socketEmit('new-timeout', { action: 'new timeout' }); - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 100000, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'TIMEOUT', - target_user_id: userId, - coins_amount: -100000, - user_new_amount: commandUser.coins - 100000, - }) - await emitDataUpdated({ table: 'users', action: 'update' }); - - try { - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${commandUserId}> a time-out <@${userId}> pour 12h`) - .setColor('#5865f2') - .setTimestamp(new Date()); - - await generalChannel.send({ embeds: [embed] }); - } catch (e) { - console.log(e) - } - - return res.status(200).json({ message: `${user.globalName} est maintenant time-out pour 12h`}) - }); - - // --- Prediction Routes --- - - router.get('/predis', (req, res) => { - const reversedPredis = Object.fromEntries(Object.entries(activePredis).reverse()); - res.status(200).json({ predis: reversedPredis }); - }); - - router.post('/start-predi', async (req, res) => { - let { commandUserId, label, options, closingTime, payoutTime } = req.body - - const commandUser = getUser.get(commandUserId) - - if (!commandUser) return res.status(403).send({ message: 'Oups petit problème'}) - if (commandUser.coins < 100) return res.status(403).send({ message: 'Tu n\'as pas assez de FlopoCoins'}) - - if (Object.values(activePredis).find(p => p.creatorId === commandUserId && (p.endTime > Date.now() && !p.closed))) { - return res.status(403).json({ message: `Tu ne peux pas lancer plus d'une prédi à la fois !`}) - } - - const startTime = Date.now() - const newPrediId = commandUserId?.toString() + '-' + startTime?.toString() - - let msgId; - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setTitle(`Prédiction de ${commandUser.username}`) - .setDescription(`**${label}**`) - .addFields( - { name: `${options[0]}`, value: ``, inline: true }, - { name: ``, value: `ou`, inline: true }, - { name: `${options[1]}`, value: ``, inline: true } - ) - .setFooter({ text: `${formatTime(closingTime).replaceAll('*', '')} pour voter` }) - .setColor('#5865f2') - .setTimestamp(new Date()); - - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`option_0_${newPrediId}`) - .setLabel(`+10 sur '${options[0]}'`) - .setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId(`option_1_${newPrediId}`) - .setLabel(`+10 sur '${options[1]}'`) - .setStyle(ButtonStyle.Primary) - ); - - const row2 = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setLabel('Voter sur FlopoSite') - .setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/dashboard`) - .setStyle(ButtonStyle.Link) - ) - - const msg = await generalChannel.send({ embeds: [embed], components: [/*row,*/ row2] }); - msgId = msg.id; - } catch (e) { - console.log(e) - return res.status(500).send({ message: 'Erreur lors de l\'envoi du message'}) - } - - const formattedOptions = [ - { label: options[0], votes: [], total: 0, percent: 0, }, - { label: options[1], votes: [], total: 0, percent: 0, }, - ] - activePredis[newPrediId] = { - creatorId: commandUserId, - label: label, - options: formattedOptions, - startTime: startTime, - closingTime: startTime + (closingTime * 1000), - endTime: startTime + (closingTime * 1000) + (payoutTime * 1000), - closed: false, - winning: null, - cancelledTime: null, - paidTime: null, - msgId: msgId, - }; - await socketEmit('new-predi', { action: 'new predi' }); - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 100, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'START_PREDI', - target_user_id: null, - coins_amount: -100, - user_new_amount: commandUser.coins - 100, - }) - await emitDataUpdated({ table: 'users', action: 'update' }); - - return res.status(200).json({ message: `Ta prédi '${label}' a commencée !`}) - }); - - router.post('/vote-predi', async (req, res) => { - const { commandUserId, predi, amount, option } = req.body - - let warning = false; - - let intAmount = parseInt(amount) - if (intAmount < 10 || intAmount > 250000) return res.status(403).send({ message: 'Montant invalide'}) - - const commandUser = getUser.get(commandUserId) - if (!commandUser) return res.status(403).send({ message: 'Oups, je ne te connais pas'}) - if (commandUser.coins < intAmount) return res.status(403).send({ message: 'Tu n\'as pas assez de FlopoCoins'}) - - const prediObject = activePredis[predi] - if (!prediObject) return res.status(403).send({ message: 'Prédiction introuvable'}) - - if (prediObject.endTime < Date.now()) return res.status(403).send({ message: 'Les votes de cette prédiction sont clos'}) - - const otherOption = option === 0 ? 1 : 0; - if (prediObject.options[otherOption].votes.find(v => v.id === commandUserId) && commandUserId !== process.env.DEV_ID) return res.status(403).send({ message: 'Tu ne peux pas voter pour les 2 deux options'}) - - if (prediObject.options[option].votes.find(v => v.id === commandUserId)) { - activePredis[predi].options[option].votes.forEach(v => { - if (v.id === commandUserId) { - if (v.amount === 250000) { - return res.status(403).send({ message: 'Tu as déjà parié le max (250K)'}) - } - if (v.amount + intAmount > 250000) { - intAmount = 250000-v.amount - warning = true - } - v.amount += intAmount - } - }) - } else { - activePredis[predi].options[option].votes.push({ - id: commandUserId, - amount: intAmount, - }) - } - activePredis[predi].options[option].total += intAmount - - activePredis[predi].options[option].percent = (activePredis[predi].options[option].total / (activePredis[predi].options[otherOption].total + activePredis[predi].options[option].total)) * 100 - activePredis[predi].options[otherOption].percent = 100 - activePredis[predi].options[option].percent - - await socketEmit('new-predi', { action: 'new vote' }); - - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - intAmount, - }) - insertLog.run({ - id: commandUserId + '-' + Date.now(), - user_id: commandUserId, - action: 'PREDI_VOTE', - target_user_id: null, - coins_amount: -intAmount, - user_new_amount: commandUser.coins - intAmount, - }) - await emitDataUpdated({ table: 'users', action: 'update' }); - - return res.status(200).send({ message : `Vote enregistré!` }); - }); - - router.post('/end-predi', async (req, res) => { - const { commandUserId, predi, confirm, winningOption } = req.body - - const commandUser = getUser.get(commandUserId) - if (!commandUser) return res.status(403).send({ message: 'Oups, je ne te connais pas'}) - if (commandUserId !== process.env.DEV_ID) return res.status(403).send({ message: 'Tu n\'as pas les permissions requises' }) - - const prediObject = activePredis[predi] - if (!prediObject) return res.status(403).send({ message: 'Prédiction introuvable'}) - if (prediObject.closed) return res.status(403).send({ message: 'Prédiction déjà close'}) - - if (!confirm) { - activePredis[predi].cancelledTime = new Date(); - activePredis[predi].options[0].votes.forEach((v) => { - const tempUser = getUser.get(v.id) - try { - updateUserCoins.run({ - id: v.id, - coins: tempUser.coins + v.amount - }) - insertLog.run({ - id: v.id + '-' + Date.now(), - user_id: v.id, - action: 'PREDI_REFUND', - target_user_id: v.id, - coins_amount: v.amount, - user_new_amount: tempUser.coins + v.amount, - }) - } catch (e) { - console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`) - } - }) - activePredis[predi].options[1].votes.forEach((v) => { - const tempUser = getUser.get(v.id) - try { - updateUserCoins.run({ - id: v.id, - coins: tempUser.coins + v.amount - }) - insertLog.run({ - id: v.id + '-' + Date.now(), - user_id: v.id, - action: 'PREDI_REFUND', - target_user_id: v.id, - coins_amount: v.amount, - user_new_amount: tempUser.coins + v.amount, - }) - } catch (e) { - console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`) - } - }) - activePredis[predi].closed = true; - } - else { - const losingOption = winningOption === 0 ? 1 : 0; - activePredis[predi].options[winningOption].votes.forEach((v) => { - const tempUser = getUser.get(v.id) - const ratio = activePredis[predi].options[winningOption].total === 0 ? 0 : activePredis[predi].options[losingOption].total / activePredis[predi].options[winningOption].total - try { - updateUserCoins.run({ - id: v.id, - coins: tempUser.coins + (v.amount * (1 + ratio)) - }) - insertLog.run({ - id: v.id + '-' + Date.now(), - user_id: v.id, - action: 'PREDI_RESULT', - target_user_id: v.id, - coins_amount: v.amount * (1 + ratio), - user_new_amount: tempUser.coins + (v.amount * (1 + ratio)), - }) - } catch (e) { - console.log(`Impossible de créditer ${v.id} (${v.amount} coins pariés, *${1 + ratio})`) - } - }) - activePredis[predi].paidTime = new Date(); - activePredis[predi].closed = true; - activePredis[predi].winning = winningOption; - } - - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const message = await generalChannel.messages.fetch(activePredis[predi].msgId) - const updatedEmbed = new EmbedBuilder() - .setTitle(`Prédiction de ${commandUser.username}`) - .setDescription(`**${activePredis[predi].label}**`) - .setFields({ name: `${activePredis[predi].options[0].label}`, value: ``, inline: true }, - { name: ``, value: `ou`, inline: true }, - { name: `${activePredis[predi].options[1].label}`, value: ``, inline: true }, - ) - .setFooter({ text: `${activePredis[predi].cancelledTime !== null ? 'Prédi annulée' : 'Prédi confirmée !' }` }) - .setTimestamp(new Date()); - const row = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setLabel('Voir') - .setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/dashboard`) - .setStyle(ButtonStyle.Link) - ) - await message.edit({ embeds: [updatedEmbed], components: [row] }); - } catch (err) { - console.error('Error updating prédi message:', err); - } - - await socketEmit('new-predi', { action: 'closed predi' }); - await emitDataUpdated({ table: 'users', action: 'fin predi' }); - - return res.status(200).json({ message: 'Prédi close' }); - }); - - // --- Admin Routes --- - - router.post('/buy-coins', (req, res) => { - const { commandUserId, coins } = req.body; - const user = getUser.get(commandUserId); - if (!user) return res.status(404).json({ error: 'User not found' }); - - const newCoins = user.coins + coins; - updateUserCoins.run({ id: commandUserId, coins: newCoins }); - insertLog.run({ - id: `${commandUserId}-buycoins-${Date.now()}`, user_id: commandUserId, action: 'BUY_COINS_ADMIN', - coins_amount: coins, user_new_amount: newCoins - }); - - res.status(200).json({ message: `Added ${coins} coins.` }); - }); - - return router; -} \ No newline at end of file + // --- Server Health & Basic Data --- + + router.get("/check", (req, res) => { + res.status(200).json({ status: "OK", message: "FlopoBot API is running." }); + }); + + router.get("/users", (req, res) => { + try { + const users = getAllUsers.all(); + res.json(users); + } catch (error) { + console.error("Error fetching users:", error); + res.status(500).json({ error: "Failed to fetch users." }); + } + }); + + router.get("/akhys", (req, res) => { + try { + const akhys = getAllAkhys.all(); + res.json(akhys); + } catch (error) { + console.error("Error fetching akhys:", error); + res.status(500).json({ error: "Failed to fetch akhys" }); + } + }); + + router.post("/register-user", async (req, res) => { + const { discordUserId } = req.body; + const discordUser = await client.users.fetch(discordUserId); + + try { + insertUser.run({ + id: discordUser.id, + username: discordUser.username, + globalName: discordUser.globalName, + warned: 0, + warns: 0, + allTimeWarns: 0, + totalRequests: 0, + avatarUrl: discordUser.displayAvatarURL({ dynamic: true, size: 256 }), + isAkhy: 0, + }); + + updateUserCoins.run({ id: discordUser.id, coins: 5000 }); + insertLog.run({ + id: `${discordUser.id}-welcome-${Date.now()}`, + user_id: discordUser.id, + action: "WELCOME_BONUS", + target_user_id: null, + coins_amount: 5000, + user_new_amount: 5000, + }); + + console.log(`New registered user: ${discordUser.username} (${discordUser.id})`); + + res.status(200).json({ message: `Bienvenue ${discordUser.username} !` }); + } catch (e) { + console.log(`Failed to register user ${discordUser.username} (${discordUser.id})`, e); + res.status(500).json({ error: "Erreur lors de la création du nouvel utilisateur." }); + } + }); + + router.get("/skins", (req, res) => { + try { + res.json(skins); + } catch (error) { + console.error("Error fetching skins:", error); + res.status(500).json({ error: "Failed to fetch skins." }); + } + }); + + router.get("/skin/:id", (req, res) => { + try { + const skinData = skins.find((s) => s.uuid === req.params.id); + res.json(skinData); + } catch (error) { + console.error("Error fetching skin:", error); + res.status(500).json({ error: "Failed to fetch skin." }); + } + }); + + router.post("/skin/:id", (req, res) => { + const { level, chroma } = req.body; + try { + const skinData = skins.find((s) => s.uuid === req.params.id); + if (!skinData) res.status(404).json({ error: "Invalid skin." }); + + const levelData = skinData.levels[level - 1] || {}; + const chromaData = skinData.chromas[chroma - 1] || {}; + + let videoUrl = null; + if (level === skinData.levels.length) { + videoUrl = chromaData.streamedVideo; + } + videoUrl = videoUrl || levelData.streamedVideo; + + res.json({ url: videoUrl }); + } catch (error) { + console.error("Error fetching skins:", error); + res.status(500).json({ error: "Failed to fetch skins." }); + } + }); + + router.get("/users/by-elo", (req, res) => { + try { + const users = getUsersByElo.all(); + res.json(users); + } catch (error) { + console.error("Error fetching users by Elo:", error); + res.status(500).json({ error: "Failed to fetch users by Elo." }); + } + }); + + router.get("/logs", async (req, res) => { + try { + await pruneOldLogs(); + const logs = getLogs.all(); + res.status(200).json(logs); + } catch (error) { + console.error("Error fetching logs:", error); + res.status(500).json({ error: "Failed to fetch logs." }); + } + }); + + // --- User-Specific Routes --- + + router.get("/user/:id/avatar", async (req, res) => { + try { + const user = await client.users.fetch(req.params.id); + const avatarUrl = user.displayAvatarURL({ format: "png", size: 256 }); + res.json({ avatarUrl }); + } catch (error) { + res.status(404).json({ error: "User not found or failed to fetch avatar." }); + } + }); + + router.get("/user/:id/username", async (req, res) => { + try { + const user = await client.users.fetch(req.params.id); + res.json({ user }); + } catch (error) { + res.status(404).json({ error: "User not found." }); + } + }); + + router.get("/user/:id/sparkline", (req, res) => { + try { + const logs = getUserLogs.all({ user_id: req.params.id }); + res.json({ sparkline: logs }); + } catch (error) { + res.status(500).json({ error: "Failed to fetch logs for sparkline." }); + } + }); + + router.get("/user/:id/elo", (req, res) => { + try { + const eloData = getUserElo.get({ id: req.params.id }); + res.json({ elo: eloData?.elo || null }); + } catch (e) { + res.status(500).json({ error: "Failed to fetch Elo data." }); + } + }); + + router.get("/user/:id/elo-graph", (req, res) => { + try { + const games = getUserGames.all({ user_id: req.params.id }); + const eloHistory = games + .filter((game) => game.p2 !== null) + .map((game) => (game.p1 === req.params.id ? game.p1_new_elo : game.p2_new_elo)); + eloHistory.splice(0, 0, 1000); + res.json({ elo_graph: eloHistory }); + } catch (e) { + res.status(500).json({ error: "Failed to generate Elo graph." }); + } + }); + + router.get("/user/:id/inventory", (req, res) => { + try { + const inventory = getUserInventory.all({ user_id: req.params.id }); + res.json({ inventory }); + } catch (error) { + res.status(500).json({ error: "Failed to fetch inventory." }); + } + }); + + router.get("/user/:id/games-history", async (req, res) => { + try { + const games = getUserGames.all({ user_id: req.params.id }); + res.json({ games }); + } catch (err) { + res.status(500).json({ error: "Failed to fetch games history." }); + } + }); + + router.get("/user/:id/daily", async (req, res) => { + const { id } = req.params; + try { + const akhy = getUser.get(id); + if (!akhy) return res.status(404).json({ message: "Utilisateur introuvable" }); + if (akhy.dailyQueried) return res.status(403).json({ message: "Récompense journalière déjà récupérée." }); + + const amount = 500; + const newCoins = akhy.coins + amount; + queryDailyReward.run(id); + updateUserCoins.run({ id, coins: newCoins }); + insertLog.run({ + id: `${id}-daily-${Date.now()}`, + user_id: id, + action: "DAILY_REWARD", + target_user_id: null, + coins_amount: amount, + user_new_amount: newCoins, + }); + + await socketEmit("daily-queried", { userId: id }); + res.status(200).json({ message: `+${amount} FlopoCoins! Récompense récupérée !` }); + } catch (error) { + console.log(); + res.status(500).json({ error: "Failed to process daily reward." }); + } + }); + + // --- Poll & Timeout Routes --- + + router.get("/polls", (req, res) => { + res.json({ activePolls }); + }); + + router.post("/timedout", async (req, res) => { + try { + const { userId } = req.body; + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const member = await guild.members.fetch(userId); + res.status(200).json({ isTimedOut: member?.isCommunicationDisabled() || false }); + } catch (e) { + res.status(404).send({ message: "Member not found or guild unavailable." }); + } + }); + + // --- Shop & Interaction Routes --- + + router.post("/change-nickname", async (req, res) => { + const { userId, nickname, commandUserId } = req.body; + const commandUser = getUser.get(commandUserId); + if (!commandUser) return res.status(404).json({ message: "Command user not found." }); + if (commandUser.coins < 1000) return res.status(403).json({ message: "Pas assez de FlopoCoins (1000 requis)." }); + + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const member = await guild.members.fetch(userId); + const old_nickname = member.nickname; + await member.setNickname(nickname); + + const newCoins = commandUser.coins - 1000; + updateUserCoins.run({ id: commandUserId, coins: newCoins }); + insertLog.run({ + id: `${commandUserId}-changenick-${Date.now()}`, + user_id: commandUserId, + action: "CHANGE_NICKNAME", + target_user_id: userId, + coins_amount: -1000, + user_new_amount: newCoins, + }); + + console.log(`${commandUserId} change nickname of ${userId}: ${old_nickname} -> ${nickname}`); + + try { + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${commandUserId}> a modifié le pseudo de <@${userId}>`) + .addFields( + { name: `${old_nickname}`, value: ``, inline: true }, + { name: `➡️`, value: ``, inline: true }, + { name: `${nickname}`, value: ``, inline: true }, + ) + .setColor("#5865f2") + .setTimestamp(new Date()); + + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.log(e); + } + + res.status(200).json({ + message: `Le pseudo de ${member.user.username} a été changé.`, + }); + } catch (error) { + res.status(500).json({ message: `Erreur: Impossible de changer le pseudo.` }); + } + }); + + router.post("/spam-ping", async (req, res) => { + const { userId, commandUserId } = req.body; + + const user = getUser.get(userId); + const commandUser = getUser.get(commandUserId); + + if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" }); + + if (commandUser.coins < 5000) return res.status(403).json({ message: "Pas assez de coins" }); + + try { + const discordUser = await client.users.fetch(userId); + + await discordUser.send(`<@${userId}>`); + + res.status(200).json({ message: "C'est parti ehehe" }); + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - 5000, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "SPAM_PING", + target_user_id: userId, + coins_amount: -5000, + user_new_amount: commandUser.coins - 5000, + }); + await emitDataUpdated({ table: "users", action: "update" }); + + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${commandUserId}> a envoyé un spam ping à <@${userId}>`) + .setColor("#5865f2") + .setTimestamp(new Date()); + + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.log(e); + } + + for (let i = 1; i < 120; i++) { + await discordUser.send(`<@${userId}>`); + await sleep(250); + } + } catch (err) { + console.log(err); + res.status(500).json({ message: "Oups ça n'a pas marché" }); + } + }); + + // --- Slowmode Routes --- + + router.get("/slowmodes", (req, res) => { + res.status(200).json({ slowmodes: activeSlowmodes }); + }); + + router.post("/slowmode", async (req, res) => { + let { userId, commandUserId } = req.body; + + const user = getUser.get(userId); + const commandUser = getUser.get(commandUserId); + + if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" }); + + if (commandUser.coins < 10000) return res.status(403).json({ message: "Pas assez de coins" }); + + if (!user) return res.status(403).send({ message: "Oups petit problème" }); + + if (activeSlowmodes[userId]) { + if (userId === commandUserId) { + delete activeSlowmodes[userId]; + await socketEmit("new-slowmode", { action: "new slowmode" }); + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - 10000, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "SLOWMODE", + target_user_id: userId, + coins_amount: -10000, + user_new_amount: commandUser.coins - 10000, + }); + + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${commandUserId}> a retiré son slowmode`) + .setColor("#5865f2") + .setTimestamp(new Date()); + + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.log(e); + } + return res.status(200).json({ message: "Slowmode retiré" }); + } else { + let timeLeft = (activeSlowmodes[userId].endAt - Date.now()) / 1000; + timeLeft = + timeLeft > 60 ? (timeLeft / 60).toFixed()?.toString() + "min" : timeLeft.toFixed()?.toString() + "sec"; + return res.status(403).json({ + message: `${user.globalName} est déjà en slowmode (${timeLeft})`, + }); + } + } else if (userId === commandUserId) { + return res.status(403).json({ message: "Impossible de te mettre toi-même en slowmode" }); + } + + activeSlowmodes[userId] = { + userId: userId, + endAt: Date.now() + 60 * 60 * 1000, // 1 heure + lastMessage: null, + }; + await socketEmit("new-slowmode", { action: "new slowmode" }); + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - 10000, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "SLOWMODE", + target_user_id: userId, + coins_amount: -10000, + user_new_amount: commandUser.coins - 10000, + }); + await emitDataUpdated({ table: "users", action: "update" }); + + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${commandUserId}> a mis <@${userId}> en slowmode pendant 1h`) + .setColor("#5865f2") + .setTimestamp(new Date()); + + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.log(e); + } + + return res.status(200).json({ + message: `${user.globalName} est maintenant en slowmode pour 1h`, + }); + }); + + // --- Time-Out Route --- + + router.post("/timeout", async (req, res) => { + let { userId, commandUserId } = req.body; + + const user = getUser.get(userId); + const commandUser = getUser.get(commandUserId); + + if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" }); + + if (commandUser.coins < 100000) return res.status(403).json({ message: "Pas assez de coins" }); + + if (!user) return res.status(403).send({ message: "Oups petit problème" }); + + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const member = await guild.members.fetch(userId); + + if (userId === commandUserId) { + if ( + member && + (!member.communicationDisabledUntilTimestamp || member.communicationDisabledUntilTimestamp < Date.now()) + ) { + return res.status(403).json({ message: `Impossible de t'auto time-out` }); + } + await socketEmit("new-timeout", { action: "new slowmode" }); + + try { + const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${userId}`; + await DiscordRequest(endpointTimeout, { + method: "PATCH", + body: { + communication_disabled_until: new Date(Date.now()).toISOString(), + }, + }); + } catch (e) { + console.log(e); + return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` }); + } + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - 10000, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "TIMEOUT", + target_user_id: userId, + coins_amount: -10000, + user_new_amount: commandUser.coins - 10000, + }); + + try { + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${commandUserId}> a retiré son time-out`) + .setColor("#5865f2") + .setTimestamp(new Date()); + + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.log(e); + } + return res.status(200).json({ message: "Time-out retiré" }); + } + + if ( + member && + member.communicationDisabledUntilTimestamp && + member.communicationDisabledUntilTimestamp > Date.now() + ) { + return res.status(403).json({ message: `${user.globalName} est déjà time-out` }); + } + + try { + const timeoutUntil = new Date(Date.now() + 43200 * 1000).toISOString(); + const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${userId}`; + await DiscordRequest(endpointTimeout, { + method: "PATCH", + body: { communication_disabled_until: timeoutUntil }, + }); + } catch (e) { + console.log(e); + return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` }); + } + + await socketEmit("new-timeout", { action: "new timeout" }); + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - 100000, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "TIMEOUT", + target_user_id: userId, + coins_amount: -100000, + user_new_amount: commandUser.coins - 100000, + }); + await emitDataUpdated({ table: "users", action: "update" }); + + try { + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${commandUserId}> a time-out <@${userId}> pour 12h`) + .setColor("#5865f2") + .setTimestamp(new Date()); + + await generalChannel.send({ embeds: [embed] }); + } catch (e) { + console.log(e); + } + + return res.status(200).json({ message: `${user.globalName} est maintenant time-out pour 12h` }); + }); + + // --- Prediction Routes --- + + router.get("/predis", (req, res) => { + const reversedPredis = Object.fromEntries(Object.entries(activePredis).reverse()); + res.status(200).json({ predis: reversedPredis }); + }); + + router.post("/start-predi", async (req, res) => { + let { commandUserId, label, options, closingTime, payoutTime } = req.body; + + const commandUser = getUser.get(commandUserId); + + if (!commandUser) return res.status(403).send({ message: "Oups petit problème" }); + if (commandUser.coins < 100) return res.status(403).send({ message: "Tu n'as pas assez de FlopoCoins" }); + + if (Object.values(activePredis).find((p) => p.creatorId === commandUserId && p.endTime > Date.now() && !p.closed)) { + return res.status(403).json({ + message: `Tu ne peux pas lancer plus d'une prédi à la fois !`, + }); + } + + const startTime = Date.now(); + const newPrediId = commandUserId?.toString() + "-" + startTime?.toString(); + + let msgId; + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setTitle(`Prédiction de ${commandUser.username}`) + .setDescription(`**${label}**`) + .addFields( + { name: `${options[0]}`, value: ``, inline: true }, + { name: ``, value: `ou`, inline: true }, + { name: `${options[1]}`, value: ``, inline: true }, + ) + .setFooter({ + text: `${formatTime(closingTime).replaceAll("*", "")} pour voter`, + }) + .setColor("#5865f2") + .setTimestamp(new Date()); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`option_0_${newPrediId}`) + .setLabel(`+10 sur '${options[0]}'`) + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId(`option_1_${newPrediId}`) + .setLabel(`+10 sur '${options[1]}'`) + .setStyle(ButtonStyle.Primary), + ); + + const row2 = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Voter sur FlopoSite") + .setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/dashboard`) + .setStyle(ButtonStyle.Link), + ); + + const msg = await generalChannel.send({ + embeds: [embed], + components: [/*row,*/ row2], + }); + msgId = msg.id; + } catch (e) { + console.log(e); + return res.status(500).send({ message: "Erreur lors de l'envoi du message" }); + } + + const formattedOptions = [ + { label: options[0], votes: [], total: 0, percent: 0 }, + { label: options[1], votes: [], total: 0, percent: 0 }, + ]; + activePredis[newPrediId] = { + creatorId: commandUserId, + label: label, + options: formattedOptions, + startTime: startTime, + closingTime: startTime + closingTime * 1000, + endTime: startTime + closingTime * 1000 + payoutTime * 1000, + closed: false, + winning: null, + cancelledTime: null, + paidTime: null, + msgId: msgId, + }; + await socketEmit("new-predi", { action: "new predi" }); + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - 100, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "START_PREDI", + target_user_id: null, + coins_amount: -100, + user_new_amount: commandUser.coins - 100, + }); + await emitDataUpdated({ table: "users", action: "update" }); + + return res.status(200).json({ message: `Ta prédi '${label}' a commencée !` }); + }); + + router.post("/vote-predi", async (req, res) => { + const { commandUserId, predi, amount, option } = req.body; + + let warning = false; + + let intAmount = parseInt(amount); + if (intAmount < 10 || intAmount > 250000) return res.status(403).send({ message: "Montant invalide" }); + + const commandUser = getUser.get(commandUserId); + if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" }); + if (commandUser.coins < intAmount) return res.status(403).send({ message: "Tu n'as pas assez de FlopoCoins" }); + + const prediObject = activePredis[predi]; + if (!prediObject) return res.status(403).send({ message: "Prédiction introuvable" }); + + if (prediObject.endTime < Date.now()) + return res.status(403).send({ message: "Les votes de cette prédiction sont clos" }); + + const otherOption = option === 0 ? 1 : 0; + if ( + prediObject.options[otherOption].votes.find((v) => v.id === commandUserId) && + commandUserId !== process.env.DEV_ID + ) + return res.status(403).send({ message: "Tu ne peux pas voter pour les 2 deux options" }); + + if (prediObject.options[option].votes.find((v) => v.id === commandUserId)) { + activePredis[predi].options[option].votes.forEach((v) => { + if (v.id === commandUserId) { + if (v.amount === 250000) { + return res.status(403).send({ message: "Tu as déjà parié le max (250K)" }); + } + if (v.amount + intAmount > 250000) { + intAmount = 250000 - v.amount; + warning = true; + } + v.amount += intAmount; + } + }); + } else { + activePredis[predi].options[option].votes.push({ + id: commandUserId, + amount: intAmount, + }); + } + activePredis[predi].options[option].total += intAmount; + + activePredis[predi].options[option].percent = + (activePredis[predi].options[option].total / + (activePredis[predi].options[otherOption].total + activePredis[predi].options[option].total)) * + 100; + activePredis[predi].options[otherOption].percent = 100 - activePredis[predi].options[option].percent; + + await socketEmit("new-predi", { action: "new vote" }); + + updateUserCoins.run({ + id: commandUserId, + coins: commandUser.coins - intAmount, + }); + insertLog.run({ + id: commandUserId + "-" + Date.now(), + user_id: commandUserId, + action: "PREDI_VOTE", + target_user_id: null, + coins_amount: -intAmount, + user_new_amount: commandUser.coins - intAmount, + }); + await emitDataUpdated({ table: "users", action: "update" }); + + return res.status(200).send({ message: `Vote enregistré!` }); + }); + + router.post("/end-predi", async (req, res) => { + const { commandUserId, predi, confirm, winningOption } = req.body; + + const commandUser = getUser.get(commandUserId); + if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" }); + if (commandUserId !== process.env.DEV_ID) + return res.status(403).send({ message: "Tu n'as pas les permissions requises" }); + + const prediObject = activePredis[predi]; + if (!prediObject) return res.status(403).send({ message: "Prédiction introuvable" }); + if (prediObject.closed) return res.status(403).send({ message: "Prédiction déjà close" }); + + if (!confirm) { + activePredis[predi].cancelledTime = new Date(); + activePredis[predi].options[0].votes.forEach((v) => { + const tempUser = getUser.get(v.id); + try { + updateUserCoins.run({ + id: v.id, + coins: tempUser.coins + v.amount, + }); + insertLog.run({ + id: v.id + "-" + Date.now(), + user_id: v.id, + action: "PREDI_REFUND", + target_user_id: v.id, + coins_amount: v.amount, + user_new_amount: tempUser.coins + v.amount, + }); + } catch (e) { + console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`); + } + }); + activePredis[predi].options[1].votes.forEach((v) => { + const tempUser = getUser.get(v.id); + try { + updateUserCoins.run({ + id: v.id, + coins: tempUser.coins + v.amount, + }); + insertLog.run({ + id: v.id + "-" + Date.now(), + user_id: v.id, + action: "PREDI_REFUND", + target_user_id: v.id, + coins_amount: v.amount, + user_new_amount: tempUser.coins + v.amount, + }); + } catch (e) { + console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`); + } + }); + activePredis[predi].closed = true; + } else { + const losingOption = winningOption === 0 ? 1 : 0; + activePredis[predi].options[winningOption].votes.forEach((v) => { + const tempUser = getUser.get(v.id); + const ratio = + activePredis[predi].options[winningOption].total === 0 + ? 0 + : activePredis[predi].options[losingOption].total / activePredis[predi].options[winningOption].total; + try { + updateUserCoins.run({ + id: v.id, + coins: tempUser.coins + v.amount * (1 + ratio), + }); + insertLog.run({ + id: v.id + "-" + Date.now(), + user_id: v.id, + action: "PREDI_RESULT", + target_user_id: v.id, + coins_amount: v.amount * (1 + ratio), + user_new_amount: tempUser.coins + v.amount * (1 + ratio), + }); + } catch (e) { + console.log(`Impossible de créditer ${v.id} (${v.amount} coins pariés, *${1 + ratio})`); + } + }); + activePredis[predi].paidTime = new Date(); + activePredis[predi].closed = true; + activePredis[predi].winning = winningOption; + } + + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const message = await generalChannel.messages.fetch(activePredis[predi].msgId); + const updatedEmbed = new EmbedBuilder() + .setTitle(`Prédiction de ${commandUser.username}`) + .setDescription(`**${activePredis[predi].label}**`) + .setFields( + { + name: `${activePredis[predi].options[0].label}`, + value: ``, + inline: true, + }, + { name: ``, value: `ou`, inline: true }, + { + name: `${activePredis[predi].options[1].label}`, + value: ``, + inline: true, + }, + ) + .setFooter({ + text: `${activePredis[predi].cancelledTime !== null ? "Prédi annulée" : "Prédi confirmée !"}`, + }) + .setTimestamp(new Date()); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Voir") + .setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/dashboard`) + .setStyle(ButtonStyle.Link), + ); + await message.edit({ embeds: [updatedEmbed], components: [row] }); + } catch (err) { + console.error("Error updating prédi message:", err); + } + + await socketEmit("new-predi", { action: "closed predi" }); + await emitDataUpdated({ table: "users", action: "fin predi" }); + + return res.status(200).json({ message: "Prédi close" }); + }); + + // --- Admin Routes --- + + router.post("/buy-coins", (req, res) => { + const { commandUserId, coins } = req.body; + const user = getUser.get(commandUserId); + if (!user) return res.status(404).json({ error: "User not found" }); + + const newCoins = user.coins + coins; + updateUserCoins.run({ id: commandUserId, coins: newCoins }); + insertLog.run({ + id: `${commandUserId}-buycoins-${Date.now()}`, + user_id: commandUserId, + action: "BUY_COINS_ADMIN", + coins_amount: coins, + user_new_amount: newCoins, + }); + + res.status(200).json({ message: `Added ${coins} coins.` }); + }); + + return router; +} diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index 62d3c21..9f25d3e 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -1,342 +1,364 @@ // /routes/blackjack.js import express from "express"; import { - createBlackjackRoom, - startBetting, - dealInitial, - autoActions, - everyoneDone, - dealerPlay, - settleAll, - applyAction, - publicPlayerView, - handValue, - dealerShouldHit, draw + createBlackjackRoom, + startBetting, + dealInitial, + autoActions, + everyoneDone, + dealerPlay, + settleAll, + applyAction, + publicPlayerView, + handValue, + dealerShouldHit, + draw, } from "../../game/blackjack.js"; // Optional: hook into your DB & Discord systems if available import { getUser, updateUserCoins, insertLog } from "../../database/index.js"; import { client } from "../../bot/client.js"; -import {emitToast, emitUpdate} from "../socket.js"; -import {EmbedBuilder} from "discord.js"; +import { emitToast, emitUpdate } from "../socket.js"; +import { EmbedBuilder } from "discord.js"; export function blackjackRoutes(io) { - const router = express.Router(); + const router = express.Router(); - // --- Singleton continuous room --- - const room = createBlackjackRoom({ - minBet: 10, - maxBet: 10000, - fakeMoney: false, - decks: 6, - hitSoft17: false, // S17 (dealer stands on soft 17) if false - blackjackPayout: 1.5, // 3:2 - cutCardRatio: 0.25, - phaseDurations: { bettingMs: 10000, dealMs: 2000, playMsPerPlayer: 20000, revealMs: 1000, payoutMs: 7000 }, - animation: { dealerDrawMs: 1000 } - }); + // --- Singleton continuous room --- + const room = createBlackjackRoom({ + minBet: 10, + maxBet: 10000, + fakeMoney: false, + decks: 6, + hitSoft17: false, // S17 (dealer stands on soft 17) if false + blackjackPayout: 1.5, // 3:2 + cutCardRatio: 0.25, + phaseDurations: { + bettingMs: 10000, + dealMs: 2000, + playMsPerPlayer: 20000, + revealMs: 1000, + payoutMs: 7000, + }, + animation: { dealerDrawMs: 1000 }, + }); - const sleep = (ms) => new Promise(res => setTimeout(res, ms)); - let animatingDealer = false; + const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); + let animatingDealer = false; - async function runDealerAnimation() { - if (animatingDealer) return; - animatingDealer = true; + async function runDealerAnimation() { + if (animatingDealer) return; + animatingDealer = true; - room.status = "dealer"; - room.dealer.holeHidden = false; - await sleep(room.settings.phaseDurations.revealMs ?? 1000); - room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000); - emitUpdate("dealer-reveal", snapshot(room)); - await sleep(room.settings.phaseDurations.revealMs ?? 1000); + room.status = "dealer"; + room.dealer.holeHidden = false; + await sleep(room.settings.phaseDurations.revealMs ?? 1000); + room.phase_ends_at = Date.now() + (room.settings.phaseDurations.revealMs ?? 1000); + emitUpdate("dealer-reveal", snapshot(room)); + await sleep(room.settings.phaseDurations.revealMs ?? 1000); - while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) { - room.dealer.cards.push(draw(room.shoe)); - room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500); - emitUpdate("dealer-hit", snapshot(room)); - await sleep(room.settings.animation?.dealerDrawMs ?? 500); - } + while (dealerShouldHit(room.dealer.cards, room.settings.hitSoft17)) { + room.dealer.cards.push(draw(room.shoe)); + room.phase_ends_at = Date.now() + (room.settings.animation?.dealerDrawMs ?? 500); + emitUpdate("dealer-hit", snapshot(room)); + await sleep(room.settings.animation?.dealerDrawMs ?? 500); + } - settleAll(room); - room.status = "payout"; - room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000); - emitUpdate("payout", snapshot(room)) + settleAll(room); + room.status = "payout"; + room.phase_ends_at = Date.now() + (room.settings.phaseDurations.payoutMs ?? 10000); + emitUpdate("payout", snapshot(room)); - animatingDealer = false; - } + animatingDealer = false; + } - function autoTimeoutAFK(now) { - if (room.status !== "playing") return false; - if (!room.phase_ends_at || now < room.phase_ends_at) return false; + function autoTimeoutAFK(now) { + if (room.status !== "playing") return false; + if (!room.phase_ends_at || now < room.phase_ends_at) return false; - let changed = false; - for (const p of Object.values(room.players)) { - if (!p.inRound) continue; - const h = p.hands[p.activeHand]; - if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) { - h.surrendered = true; - h.stood = true; - h.hasActed = true; - room.leavingAfterRound[p.id] = true; // kick at end of round - emitToast({ type: "player-timeout", userId: p.id }); - changed = true; - } else if (h.hasActed && !h.stood) { - h.stood = true; - room.leavingAfterRound[p.id] = true; // kick at end of round - emitToast({ type: "player-auto-stand", userId: p.id }); - changed = true; - } - } - if (changed) emitUpdate("auto-surrender", snapshot(room)); - return changed; - } + let changed = false; + for (const p of Object.values(room.players)) { + if (!p.inRound) continue; + const h = p.hands[p.activeHand]; + if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) { + h.surrendered = true; + h.stood = true; + h.hasActed = true; + room.leavingAfterRound[p.id] = true; // kick at end of round + emitToast({ type: "player-timeout", userId: p.id }); + changed = true; + } else if (h.hasActed && !h.stood) { + h.stood = true; + room.leavingAfterRound[p.id] = true; // kick at end of round + emitToast({ type: "player-auto-stand", userId: p.id }); + changed = true; + } + } + if (changed) emitUpdate("auto-surrender", snapshot(room)); + return changed; + } - function snapshot(r) { - return { - id: r.id, - name: r.name, - status: r.status, - phase_ends_at: r.phase_ends_at, - minBet: r.minBet, - maxBet: r.maxBet, - settings: r.settings, - dealer: { cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards, total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total }, - players: Object.values(r.players).map(publicPlayerView), - shoeCount: r.shoe.length, - }; - } + function snapshot(r) { + return { + id: r.id, + name: r.name, + status: r.status, + phase_ends_at: r.phase_ends_at, + minBet: r.minBet, + maxBet: r.maxBet, + settings: r.settings, + dealer: { + cards: r.dealer.holeHidden ? [r.dealer.cards[0], "XX"] : r.dealer.cards, + total: r.dealer.holeHidden ? null : handValue(r.dealer.cards).total, + }, + players: Object.values(r.players).map(publicPlayerView), + shoeCount: r.shoe.length, + }; + } - // --- Public endpoints --- - router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) })); + // --- Public endpoints --- + router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) })); - router.post("/join", async (req, res) => { - const { userId } = req.body; - if (!userId) return res.status(400).json({ message: "userId required" }); - if (room.players[userId]) return res.status(200).json({ message: "Already here" }); + router.post("/join", async (req, res) => { + const { userId } = req.body; + if (!userId) return res.status(400).json({ message: "userId required" }); + if (room.players[userId]) return res.status(200).json({ message: "Already here" }); - const user = await client.users.fetch(userId); - const bank = getUser.get(userId)?.coins ?? 0; + const user = await client.users.fetch(userId); + const bank = getUser.get(userId)?.coins ?? 0; - room.players[userId] = { - id: userId, - globalName: user.globalName || user.username, - avatar: user.displayAvatarURL({ dynamic: true, size: 256 }), - bank, - currentBet: 0, - inRound: false, - hands: [{ cards: [], stood: false, busted: false, doubled: false, surrendered: false, hasActed: false, bet: 0 }], - activeHand: 0, - joined_at: Date.now(), - msgId: null, - totalDelta: 0, - totalBets: 0, - }; + room.players[userId] = { + id: userId, + globalName: user.globalName || user.username, + avatar: user.displayAvatarURL({ dynamic: true, size: 256 }), + bank, + currentBet: 0, + inRound: false, + hands: [ + { + cards: [], + stood: false, + busted: false, + doubled: false, + surrendered: false, + hasActed: false, + bet: 0, + }, + ], + activeHand: 0, + joined_at: Date.now(), + msgId: null, + totalDelta: 0, + totalBets: 0, + }; - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setDescription(`<@${userId}> joue au Blackjack`) - .addFields( - { - name: `Gains`, - value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`, - inline: true - }, - { - name: `Mises jouées`, - value: `**${room.players[userId].totalBets}**`, - inline: true - } - ) - .setColor('#5865f2') - .setTimestamp(new Date()); + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setDescription(`<@${userId}> joue au Blackjack`) + .addFields( + { + name: `Gains`, + value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`, + inline: true, + }, + { + name: `Mises jouées`, + value: `**${room.players[userId].totalBets}**`, + inline: true, + }, + ) + .setColor("#5865f2") + .setTimestamp(new Date()); - const msg = await generalChannel.send({ embeds: [embed] }); - room.players[userId].msgId = msg.id; - } catch (e) { - console.log(e); - } + const msg = await generalChannel.send({ embeds: [embed] }); + room.players[userId].msgId = msg.id; + } catch (e) { + console.log(e); + } - emitUpdate("player-joined", snapshot(room)); - return res.status(200).json({ message: "joined" }); - }); + emitUpdate("player-joined", snapshot(room)); + return res.status(200).json({ message: "joined" }); + }); - router.post("/leave", async (req, res) => { - const { userId } = req.body; - if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" }); + router.post("/leave", async (req, res) => { + const { userId } = req.body; + if (!userId || !room.players[userId]) return res.status(404).json({ message: "not in room" }); - try { - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const msg = await generalChannel.messages.fetch(room.players[userId].msgId); - const updatedEmbed = new EmbedBuilder() - .setDescription(`<@${userId}> a quitté la table de Blackjack.`) - .addFields( - { - name: `Gains`, - value: `**${room.players[userId].totalDelta >= 0 ? '+' + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`, - inline: true - }, - { - name: `Mises jouées`, - value: `**${room.players[userId].totalBets}**`, - inline: true - } - ) - .setColor(room.players[userId].totalDelta >= 0 ? 0x22A55B : 0xED4245) - .setTimestamp(new Date()); - await msg.edit({ embeds: [updatedEmbed], components: [] }); - } catch (e) { - console.log(e); - } + try { + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const msg = await generalChannel.messages.fetch(room.players[userId].msgId); + const updatedEmbed = new EmbedBuilder() + .setDescription(`<@${userId}> a quitté la table de Blackjack.`) + .addFields( + { + name: `Gains`, + value: `**${room.players[userId].totalDelta >= 0 ? "+" + room.players[userId].totalDelta : room.players[userId].totalDelta}** Flopos`, + inline: true, + }, + { + name: `Mises jouées`, + value: `**${room.players[userId].totalBets}**`, + inline: true, + }, + ) + .setColor(room.players[userId].totalDelta >= 0 ? 0x22a55b : 0xed4245) + .setTimestamp(new Date()); + await msg.edit({ embeds: [updatedEmbed], components: [] }); + } catch (e) { + console.log(e); + } - const p = room.players[userId]; - if (p.inRound) { - // leave after round to avoid abandoning an active bet - room.leavingAfterRound[userId] = true; - return res.status(200).json({ message: "will-leave-after-round" }); - } else { - delete room.players[userId]; - emitUpdate("player-left", snapshot(room)); - return res.status(200).json({ message: "left" }); - } - }); + const p = room.players[userId]; + if (p.inRound) { + // leave after round to avoid abandoning an active bet + room.leavingAfterRound[userId] = true; + return res.status(200).json({ message: "will-leave-after-round" }); + } else { + delete room.players[userId]; + emitUpdate("player-left", snapshot(room)); + return res.status(200).json({ message: "left" }); + } + }); - router.post("/bet", (req, res) => { - const { userId, amount } = req.body; - const p = room.players[userId]; - if (!p) return res.status(404).json({ message: "not in room" }); - if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" }); + router.post("/bet", (req, res) => { + const { userId, amount } = req.body; + const p = room.players[userId]; + if (!p) return res.status(404).json({ message: "not in room" }); + if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" }); - const bet = Math.floor(Number(amount) || 0); - if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" }); + const bet = Math.floor(Number(amount) || 0); + if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" }); - if (!room.settings.fakeMoney) { - const userDB = getUser.get(userId); - const coins = userDB?.coins ?? 0; - if (coins < bet) return res.status(403).json({ message: "insufficient-funds" }); - updateUserCoins.run({ id: userId, coins: coins - bet }); - insertLog.run({ - id: `${userId}-blackjack-${Date.now()}`, - user_id: userId, target_user_id: null, - action: 'BLACKJACK_BET', - coins_amount: -bet, user_new_amount: coins - bet, - }); - p.bank = coins - bet; - } + if (!room.settings.fakeMoney) { + const userDB = getUser.get(userId); + const coins = userDB?.coins ?? 0; + if (coins < bet) return res.status(403).json({ message: "insufficient-funds" }); + updateUserCoins.run({ id: userId, coins: coins - bet }); + insertLog.run({ + id: `${userId}-blackjack-${Date.now()}`, + user_id: userId, + target_user_id: null, + action: "BLACKJACK_BET", + coins_amount: -bet, + user_new_amount: coins - bet, + }); + p.bank = coins - bet; + } - p.currentBet = bet; - p.hands[p.activeHand].bet = bet; - emitToast({ type: "player-bet", userId, amount: bet }); - emitUpdate("bet-placed", snapshot(room)); - return res.status(200).json({ message: "bet-accepted" }); - }); + p.currentBet = bet; + p.hands[p.activeHand].bet = bet; + emitToast({ type: "player-bet", userId, amount: bet }); + emitUpdate("bet-placed", snapshot(room)); + return res.status(200).json({ message: "bet-accepted" }); + }); - router.post("/action/:action", (req, res) => { - const { userId } = req.body; - const action = req.params.action; - const p = room.players[userId]; - if (!p) return res.status(404).json({ message: "not in room" }); - if (!p.inRound || room.status !== "playing") return res.status(403).json({ message: "not-your-turn" }); + router.post("/action/:action", (req, res) => { + const { userId } = req.body; + const action = req.params.action; + const p = room.players[userId]; + if (!p) return res.status(404).json({ message: "not in room" }); + if (!p.inRound || room.status !== "playing") return res.status(403).json({ message: "not-your-turn" }); - // Handle extra coin lock for double - if (action === "double" && !room.settings.fakeMoney) { - const userDB = getUser.get(userId); - const coins = userDB?.coins ?? 0; - const hand = p.hands[p.activeHand]; - if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" }); - updateUserCoins.run({ id: userId, coins: coins - hand.bet }); - insertLog.run({ - id: `${userId}-blackjack-${Date.now()}`, - user_id: userId, target_user_id: null, - action: 'BLACKJACK_DOUBLE', - coins_amount: -hand.bet, user_new_amount: coins - hand.bet, - }); - p.bank = coins - hand.bet; - // effective bet size is handled in settlement via hand.doubled flag - } + // Handle extra coin lock for double + if (action === "double" && !room.settings.fakeMoney) { + const userDB = getUser.get(userId); + const coins = userDB?.coins ?? 0; + const hand = p.hands[p.activeHand]; + if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" }); + updateUserCoins.run({ id: userId, coins: coins - hand.bet }); + insertLog.run({ + id: `${userId}-blackjack-${Date.now()}`, + user_id: userId, + target_user_id: null, + action: "BLACKJACK_DOUBLE", + coins_amount: -hand.bet, + user_new_amount: coins - hand.bet, + }); + p.bank = coins - hand.bet; + // effective bet size is handled in settlement via hand.doubled flag + } - if (action === "split" && !room.settings.fakeMoney) { - const userDB = getUser.get(userId); - const coins = userDB?.coins ?? 0; - const hand = p.hands[p.activeHand]; - if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" }); - updateUserCoins.run({ id: userId, coins: coins - hand.bet }); - insertLog.run({ - id: `${userId}-blackjack-${Date.now()}`, - user_id: userId, target_user_id: null, - action: 'BLACKJACK_SPLIT', - coins_amount: -hand.bet, user_new_amount: coins - hand.bet, - }); - p.bank = coins - hand.bet; - // effective bet size is handled in settlement via hand.doubled flag - } + if (action === "split" && !room.settings.fakeMoney) { + const userDB = getUser.get(userId); + const coins = userDB?.coins ?? 0; + const hand = p.hands[p.activeHand]; + if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" }); + updateUserCoins.run({ id: userId, coins: coins - hand.bet }); + insertLog.run({ + id: `${userId}-blackjack-${Date.now()}`, + user_id: userId, + target_user_id: null, + action: "BLACKJACK_SPLIT", + coins_amount: -hand.bet, + user_new_amount: coins - hand.bet, + }); + p.bank = coins - hand.bet; + // effective bet size is handled in settlement via hand.doubled flag + } - try { - const evt = applyAction(room, userId, action); - emitToast({ type: `player-${evt}`, userId }); - emitUpdate("player-action", snapshot(room)); - return res.status(200).json({ message: "ok" }); - } catch (e) { - return res.status(400).json({ message: e.message }); - } - }); + try { + const evt = applyAction(room, userId, action); + emitToast({ type: `player-${evt}`, userId }); + emitUpdate("player-action", snapshot(room)); + return res.status(200).json({ message: "ok" }); + } catch (e) { + return res.status(400).json({ message: e.message }); + } + }); - // --- Game loop --- - // Simple phase machine that runs regardless of player count. - setInterval(async () => { - const now = Date.now(); + // --- Game loop --- + // Simple phase machine that runs regardless of player count. + setInterval(async () => { + const now = Date.now(); - if (room.status === "betting" && now >= room.phase_ends_at) { - const hasBets = Object.values(room.players).some(p => p.currentBet >= room.minBet); - if (!hasBets) { - // Extend betting window if no one bet - room.phase_ends_at = now + room.settings.phaseDurations.bettingMs; - emitUpdate("betting-extend", snapshot(room)); - return; - } - dealInitial(room); - autoActions(room); - emitUpdate("initial-deal", snapshot(room)); + if (room.status === "betting" && now >= room.phase_ends_at) { + const hasBets = Object.values(room.players).some((p) => p.currentBet >= room.minBet); + if (!hasBets) { + // Extend betting window if no one bet + room.phase_ends_at = now + room.settings.phaseDurations.bettingMs; + emitUpdate("betting-extend", snapshot(room)); + return; + } + dealInitial(room); + autoActions(room); + emitUpdate("initial-deal", snapshot(room)); - room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer; - emitUpdate("playing-start", snapshot(room)); - return; - } + room.phase_ends_at = Date.now() + room.settings.phaseDurations.playMsPerPlayer; + emitUpdate("playing-start", snapshot(room)); + return; + } - if (room.status === "playing") { - // If the per-round playing timer expired, auto-surrender AFKs (you already added this) - if (room.phase_ends_at && now >= room.phase_ends_at) { - autoTimeoutAFK(now); - } + if (room.status === "playing") { + // If the per-round playing timer expired, auto-surrender AFKs (you already added this) + if (room.phase_ends_at && now >= room.phase_ends_at) { + autoTimeoutAFK(now); + } - // Everyone acted before the timer? Cut short and go straight to dealer. - if (everyoneDone(room) && !animatingDealer) { - // Set a new server-driven deadline for the reveal pause, - // so the client's countdown immediately reflects the phase change. - room.phase_ends_at = Date.now(); - emitUpdate("playing-cut-short", snapshot(room)); + // Everyone acted before the timer? Cut short and go straight to dealer. + if (everyoneDone(room) && !animatingDealer) { + // Set a new server-driven deadline for the reveal pause, + // so the client's countdown immediately reflects the phase change. + room.phase_ends_at = Date.now(); + emitUpdate("playing-cut-short", snapshot(room)); - // Now run the animated dealer with per-step updates - runDealerAnimation(); - } - } + // Now run the animated dealer with per-step updates + runDealerAnimation(); + } + } - if (room.status === "payout" && now >= room.phase_ends_at) { - // Remove leavers - for (const userId of Object.keys(room.leavingAfterRound)) { - delete room.players[userId]; - } - // Prepare next round - startBetting(room, now); - emitUpdate("new-round", snapshot(room)); - } - }, 100); + if (room.status === "payout" && now >= room.phase_ends_at) { + // Remove leavers + for (const userId of Object.keys(room.leavingAfterRound)) { + delete room.players[userId]; + } + // Prepare next round + startBetting(room, now); + emitUpdate("new-round", snapshot(room)); + } + }, 100); - return router; -} \ No newline at end of file + return router; +} diff --git a/src/server/routes/erinyes.js b/src/server/routes/erinyes.js index 025ebca..d4c5b51 100644 --- a/src/server/routes/erinyes.js +++ b/src/server/routes/erinyes.js @@ -1,7 +1,7 @@ import express from "express"; -import { v4 as uuidv4 } from 'uuid'; -import {erinyesRooms} from "../../game/state.js"; -import {socketEmit} from "../socket.js"; +import { v4 as uuidv4 } from "uuid"; +import { erinyesRooms } from "../../game/state.js"; +import { socketEmit } from "../socket.js"; const router = express.Router(); @@ -12,89 +12,91 @@ const router = express.Router(); * @returns {object} The configured Express router. */ export function erinyesRoutes(client, io) { + // --- Router Management Endpoints - // --- Router Management Endpoints + router.get("/", (req, res) => { + res.status(200).json({ rooms: erinyesRooms }); + }); - router.get('/', (req, res) => { - res.status(200).json({ rooms: erinyesRooms }) - }) + router.get("/:id", (req, res) => { + const room = erinyesRooms[req.params.id]; + if (room) { + res.status(200).json({ room }); + } else { + res.status(404).json({ message: "Room not found." }); + } + }); - router.get('/:id', (req, res) => { - const room = erinyesRooms[req.params.id]; - if (room) { - res.status(200).json({ room }); - } else { - res.status(404).json({ message: 'Room not found.' }); - } - }) + router.post("/create", async (req, res) => { + const { creatorId } = req.body; + if (!creatorId) return res.status(404).json({ message: "Creator ID is required." }); - router.post('/create', async (req, res) => { - const { creatorId } = req.body; - if (!creatorId) return res.status(404).json({ message: 'Creator ID is required.' }); + if (Object.values(erinyesRooms).some((room) => creatorId === room.host_id || room.players[creatorId])) { + res.status(404).json({ message: "You are already in a room." }); + } - if (Object.values(erinyesRooms).some(room => creatorId === room.host_id || room.players[creatorId])) { - res.status(404).json({ message: 'You are already in a room.' }); - } + const creator = await client.users.fetch(creatorId); + const id = uuidv4(); - const creator = await client.users.fetch(creatorId); - const id = uuidv4() + createRoom({ + host_id: creatorId, + host_name: creator.globalName, + game_rules: {}, // Specific game rules + roles: [], // Every role in the game + }); - createRoom({ - host_id: creatorId, - host_name: creator.globalName, - game_rules: {}, // Specific game rules - roles: [], // Every role in the game - }) + await socketEmit("erinyes-update", { + room: erinyesRooms[id], + type: "room-created", + }); + res.status(200).json({ room: id }); + }); - await socketEmit('erinyes-update', { room: erinyesRooms[id], type: 'room-created' }); - res.status(200).json({ room: id }); - }) - - return router; + return router; } function createRoom(config) { - erinyesRooms[config.id] = { - host_id: config.host_id, - host_name: config.host_name, - created_at: Date.now(), - last_move_at: null, - players: {}, - current_player: null, - current_turn: null, - playing: false, - game_rules: createGameRules(config.game_rules), - roles: config.roles, - roles_rules: createRolesRules(config.roles), - bonuses: {} - } + erinyesRooms[config.id] = { + host_id: config.host_id, + host_name: config.host_name, + created_at: Date.now(), + last_move_at: null, + players: {}, + current_player: null, + current_turn: null, + playing: false, + game_rules: createGameRules(config.game_rules), + roles: config.roles, + roles_rules: createRolesRules(config.roles), + bonuses: {}, + }; } function createGameRules(config) { - return { - day_vote_time: config.day_vote_time ?? 60000, - // ... - }; + return { + day_vote_time: config.day_vote_time ?? 60000, + // ... + }; } function createRolesRules(roles) { - const roles_rules = {} + const roles_rules = {}; - roles.forEach(role => { - switch (role) { - case 'erynie': - roles_rules[role] = { - //... - }; - break; - //... - default: - roles_rules[role] = { - //... - }; - break; - } - }) + roles.forEach((role) => { + switch (role) { + case "erynie": + roles_rules[role] = { + //... + }; + break; + //... + default: + roles_rules[role] = { + //... + }; + break; + } + }); - return roles_rules; -} \ No newline at end of file + return roles_rules; +} diff --git a/src/server/routes/market.js b/src/server/routes/market.js new file mode 100644 index 0000000..8b55d30 --- /dev/null +++ b/src/server/routes/market.js @@ -0,0 +1,73 @@ +import express from "express"; + +// --- Database Imports --- +// --- Game State Imports --- +// --- Utility and API Imports --- +// --- Discord.js Builder Imports --- +import { ButtonStyle } from "discord.js"; +import { getMarketOfferById, getMarketOffers, getOfferBids } from "../../database/index.js"; + +// Create a new router instance +const router = express.Router(); + +/** + * Factory function to create and configure the market routes. + * @param {object} client - The Discord.js client instance. + * @param {object} io - The Socket.IO server instance. + * @returns {object} The configured Express router. + */ +export function marketRoutes(client, io) { + router.get("/offers", async (req, res) => { + try { + const offers = getMarketOffers.all(); + res.status(200).send({ offers }); + } catch (e) { + res.status(500).send({ error: e }); + } + }); + + router.get("/offers/:id", async (req, res) => { + try { + const offer = getMarketOfferById.get(req.params.id); + if (offer) { + res.status(200).send({ offer }); + } else { + res.status(404).send({ error: "Offer not found" }); + } + } catch (e) { + res.status(500).send({ error: e }); + } + }); + + router.get("/offers/:id/bids", async (req, res) => { + try { + // Placeholder for fetching bids logic + const bids = getOfferBids.get(req.params.id); + res.status(200).send({ bids }); + } catch (e) { + res.status(500).send({ error: e }); + } + }); + + router.post("/place-offer", async (req, res) => { + try { + // Placeholder for placing an offer logic + // Extract data from req.body and process accordingly + res.status(200).send({ message: "Offer placed successfully" }); + } catch (e) { + res.status(500).send({ error: e }); + } + }); + + router.post("/offers/:id/place-bid", async (req, res) => { + try { + // Placeholder for placing a bid logic + // Extract data from req.body and process accordingly + res.status(200).send({ message: "Bid placed successfully" }); + } catch (e) { + res.status(500).send({ error: e }); + } + }); + + return router; +} diff --git a/src/server/routes/poker.js b/src/server/routes/poker.js index b405990..529da01 100644 --- a/src/server/routes/poker.js +++ b/src/server/routes/poker.js @@ -1,18 +1,24 @@ -import express from 'express'; -import { v4 as uuidv4 } from 'uuid'; -import { uniqueNamesGenerator, adjectives } from 'unique-names-generator'; -import pkg from 'pokersolver'; +import express from "express"; +import { v4 as uuidv4 } from "uuid"; +import { uniqueNamesGenerator, adjectives } from "unique-names-generator"; +import pkg from "pokersolver"; const { Hand } = pkg; -import { pokerRooms } from '../../game/state.js'; -import { initialShuffledCards, getFirstActivePlayerAfterDealer, getNextActivePlayer, checkEndOfBettingRound, checkRoomWinners } from '../../game/poker.js'; -import { pokerEloHandler } from '../../game/elo.js'; -import { getUser, updateUserCoins, insertLog } from '../../database/index.js'; +import { pokerRooms } from "../../game/state.js"; +import { + initialShuffledCards, + getFirstActivePlayerAfterDealer, + getNextActivePlayer, + checkEndOfBettingRound, + checkRoomWinners, +} from "../../game/poker.js"; +import { pokerEloHandler } from "../../game/elo.js"; +import { getUser, updateUserCoins, insertLog } from "../../database/index.js"; import { sleep } from "openai/core"; -import {client} from "../../bot/client.js"; -import {emitPokerToast, emitPokerUpdate} from "../socket.js"; -import {ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder} from "discord.js"; -import {formatAmount} from "../../utils/index.js"; +import { client } from "../../bot/client.js"; +import { emitPokerToast, emitPokerUpdate } from "../socket.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; +import { formatAmount } from "../../utils/index.js"; const router = express.Router(); @@ -23,474 +29,540 @@ const router = express.Router(); * @returns {object} The configured Express router. */ export function pokerRoutes(client, io) { + // --- Room Management Endpoints --- - // --- Room Management Endpoints --- + router.get("/", (req, res) => { + res.status(200).json({ rooms: pokerRooms }); + }); - router.get('/', (req, res) => { - res.status(200).json({ rooms: pokerRooms }); - }); + router.get("/:id", (req, res) => { + const room = pokerRooms[req.params.id]; + if (room) { + res.status(200).json({ room }); + } else { + res.status(404).json({ message: "Poker room not found." }); + } + }); - router.get('/:id', (req, res) => { - const room = pokerRooms[req.params.id]; - if (room) { - res.status(200).json({ room }); - } else { - res.status(404).json({ message: 'Poker room not found.' }); - } - }); + router.post("/create", async (req, res) => { + const { creatorId, minBet, fakeMoney } = req.body; + if (!creatorId) return res.status(400).json({ message: "Creator ID is required." }); - router.post('/create', async (req, res) => { - const { creatorId, minBet, fakeMoney } = req.body; - if (!creatorId) return res.status(400).json({ message: 'Creator ID is required.' }); + if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) { + return res.status(403).json({ message: "You are already in a poker room." }); + } - if (Object.values(pokerRooms).some(room => room.host_id === creatorId || room.players[creatorId])) { - return res.status(403).json({ message: 'You are already in a poker room.' }); - } + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const creator = await client.users.fetch(creatorId); + const id = uuidv4(); + const name = uniqueNamesGenerator({ + dictionaries: [adjectives, ["Poker"]], + separator: " ", + style: "capital", + }); - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const creator = await client.users.fetch(creatorId); - const id = uuidv4(); - const name = uniqueNamesGenerator({ dictionaries: [adjectives, ['Poker']], separator: ' ', style: 'capital' }); + pokerRooms[id] = { + id, + host_id: creatorId, + host_name: creator.globalName || creator.username, + name, + created_at: Date.now(), + last_move_at: null, + players: {}, + queue: {}, + afk: {}, + pioche: initialShuffledCards(), + tapis: [], + dealer: null, + sb: null, + bb: null, + highest_bet: 0, + current_player: null, + current_turn: null, + playing: false, + winners: [], + waiting_for_restart: false, + fakeMoney: fakeMoney, + minBet: minBet, + }; - pokerRooms[id] = { - id, host_id: creatorId, host_name: creator.globalName || creator.username, - name, created_at: Date.now(), last_move_at: null, - players: {}, queue: {}, afk: {}, pioche: initialShuffledCards(), tapis: [], - dealer: null, sb: null, bb: null, highest_bet: 0, current_player: null, - current_turn: null, playing: false, winners: [], waiting_for_restart: false, fakeMoney: fakeMoney, - minBet: minBet, - }; + await joinRoom(id, creatorId, io); // Auto-join the creator + await emitPokerUpdate({ room: pokerRooms[id], type: "room-created" }); - await joinRoom(id, creatorId, io); // Auto-join the creator - await emitPokerUpdate({ room: pokerRooms[id], type: 'room-created' }); + try { + const generalChannel = guild.channels.cache.find((ch) => ch.name === "général" || ch.name === "general"); + const embed = new EmbedBuilder() + .setTitle("Flopoker 🃏") + .setDescription(`<@${creatorId}> a créé une table de poker`) + .addFields( + { name: `Nom`, value: `**${name}**`, inline: true }, + { + name: `${fakeMoney ? "Mise initiale" : "Prix d'entrée"}`, + value: `**${formatAmount(minBet)}** 🪙`, + inline: true, + }, + { + name: `Fake Money`, + value: `${fakeMoney ? "**Oui** ✅" : "**Non** ❌"}`, + inline: true, + }, + ) + .setColor("#5865f2") + .setTimestamp(new Date()); - try { - const generalChannel = guild.channels.cache.find( - ch => ch.name === 'général' || ch.name === 'general' - ); - const embed = new EmbedBuilder() - .setTitle('Flopoker 🃏') - .setDescription(`<@${creatorId}> a créé une table de poker`) - .addFields( - { name: `Nom`, value: `**${name}**`, inline: true }, - { name: `${fakeMoney ? 'Mise initiale' : 'Prix d\'entrée'}`, value: `**${formatAmount(minBet)}** 🪙`, inline: true }, - { name: `Fake Money`, value: `${fakeMoney ? '**Oui** ✅' : '**Non** ❌'}`, inline: true }, - ) - .setColor('#5865f2') - .setTimestamp(new Date()); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel(`Rejoindre la table ${name}`) + .setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`) + .setStyle(ButtonStyle.Link), + ); - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel(`Rejoindre la table ${name}`) - .setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/poker/${id}`) - .setStyle(ButtonStyle.Link) - ); + await generalChannel.send({ embeds: [embed], components: [row] }); + } catch (e) { + console.log(e); + } - await generalChannel.send({ embeds: [embed], components: [row] }); - } catch (e) { - console.log(e) - } + res.status(201).json({ roomId: id }); + }); - res.status(201).json({ roomId: id }); - }); + router.post("/join", async (req, res) => { + const { userId, roomId } = req.body; + if (!userId || !roomId) return res.status(400).json({ message: "User ID and Room ID are required." }); + if (!pokerRooms[roomId]) return res.status(404).json({ message: "Room not found." }); + if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) { + return res.status(403).json({ message: "You are already in a room or queue." }); + } + if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > (getUser.get(userId)?.coins ?? 0)) { + return res.status(403).json({ message: "You do not have enough coins to join this room." }); + } - router.post('/join', async (req, res) => { - const { userId, roomId } = req.body; - if (!userId || !roomId) return res.status(400).json({ message: 'User ID and Room ID are required.' }); - if (!pokerRooms[roomId]) return res.status(404).json({ message: 'Room not found.' }); - if (Object.values(pokerRooms).some(r => r.players[userId] || r.queue[userId])) { - return res.status(403).json({ message: 'You are already in a room or queue.' }); - } - if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > (getUser.get(userId)?.coins ?? 0)) { - return res.status(403).json({ message: 'You do not have enough coins to join this room.' }); - } + await joinRoom(roomId, userId, io); + res.status(200).json({ message: "Successfully joined." }); + }); - await joinRoom(roomId, userId, io); - res.status(200).json({ message: 'Successfully joined.' }); - }); + router.post("/accept", async (req, res) => { + const { hostId, playerId, roomId } = req.body; + const room = pokerRooms[roomId]; + if (!room || room.host_id !== hostId || !room.queue[playerId]) { + return res.status(403).json({ message: "Unauthorized or player not in queue." }); + } - router.post('/accept', async (req, res) => { - const { hostId, playerId, roomId } = req.body; - const room = pokerRooms[roomId]; - if (!room || room.host_id !== hostId || !room.queue[playerId]) { - return res.status(403).json({ message: 'Unauthorized or player not in queue.' }); - } + if (!room.fakeMoney) { + const userDB = getUser.get(playerId); + if (userDB) { + updateUserCoins.run({ + id: playerId, + coins: userDB.coins - room.minBet, + }); + insertLog.run({ + id: `${playerId}-poker-${Date.now()}`, + user_id: playerId, + target_user_id: null, + action: "POKER_JOIN", + coins_amount: -room.minBet, + user_new_amount: userDB.coins - room.minBet, + }); + } + } - if (!room.fakeMoney) { - const userDB = getUser.get(playerId); - if (userDB) { - updateUserCoins.run({ id: playerId, coins: userDB.coins - room.minBet }); - insertLog.run({ - id: `${playerId}-poker-${Date.now()}`, - user_id: playerId, target_user_id: null, - action: 'POKER_JOIN', - coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet, - }) - } - } + room.players[playerId] = room.queue[playerId]; + delete room.queue[playerId]; - room.players[playerId] = room.queue[playerId]; - delete room.queue[playerId]; + await emitPokerUpdate({ room: room, type: "player-accepted" }); + res.status(200).json({ message: "Player accepted." }); + }); - await emitPokerUpdate({ room: room, type: 'player-accepted' }); - res.status(200).json({ message: 'Player accepted.' }); - }); + router.post("/leave", async (req, res) => { + const { userId, roomId } = req.body; - router.post('/leave', async (req, res) => { - const { userId, roomId } = req.body + if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" }); + if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" }); - if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' }) - if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' }) + if ( + pokerRooms[roomId].playing && + pokerRooms[roomId].current_turn !== null && + pokerRooms[roomId].current_turn !== 4 + ) { + pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId]; - if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) { - pokerRooms[roomId].afk[userId] = pokerRooms[roomId].players[userId] + try { + pokerRooms[roomId].players[userId].folded = true; + pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn; + if (pokerRooms[roomId].current_player === userId) { + await checkRoundCompletion(pokerRooms[roomId], io); + } + } catch (e) { + console.log(e); + } - try { - pokerRooms[roomId].players[userId].folded = true - pokerRooms[roomId].players[userId].last_played_turn = pokerRooms[roomId].current_turn - if (pokerRooms[roomId].current_player === userId) { - await checkRoundCompletion(pokerRooms[roomId], io); - } - } catch(e) { - console.log(e) - } + await emitPokerUpdate({ type: "player-afk" }); + return res.status(200); + } - await emitPokerUpdate({ type: 'player-afk' }); - return res.status(200) - } + try { + updatePlayerCoins( + pokerRooms[roomId].players[userId], + pokerRooms[roomId].players[userId].bank, + pokerRooms[roomId].fakeMoney, + ); + delete pokerRooms[roomId].players[userId]; - try { - updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney); - delete pokerRooms[roomId].players[userId] + if (userId === pokerRooms[roomId].host_id) { + const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId); + if (!newHostId) { + delete pokerRooms[roomId]; + } else { + pokerRooms[roomId].host_id = newHostId; + } + } + } catch (e) { + console.log(e); + } - if (userId === pokerRooms[roomId].host_id) { - const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId) - if (!newHostId) { - delete pokerRooms[roomId] - } else { - pokerRooms[roomId].host_id = newHostId - } - } - } catch (e) { - console.log(e) - } + await emitPokerUpdate({ type: "player-left" }); + return res.status(200); + }); - await emitPokerUpdate({ type: 'player-left' }); - return res.status(200) - }); + router.post("/kick", async (req, res) => { + const { commandUserId, userId, roomId } = req.body; - router.post('/kick', async (req, res) => { - const { commandUserId, userId, roomId } = req.body + if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" }); + if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: "Joueur introuvable" }); + if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: "Seul l'host peut kick" }); + if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" }); - if (!pokerRooms[roomId]) return res.status(404).send({ message: 'Table introuvable' }) - if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: 'Joueur introuvable' }) - if (pokerRooms[roomId].host_id !== commandUserId) return res.status(403).send({ message: 'Seul l\'host peut kick' }) - if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: 'Joueur introuvable' }) + if ( + pokerRooms[roomId].playing && + pokerRooms[roomId].current_turn !== null && + pokerRooms[roomId].current_turn !== 4 + ) { + return res.status(403).send({ message: "Playing" }); + } - if (pokerRooms[roomId].playing && (pokerRooms[roomId].current_turn !== null && pokerRooms[roomId].current_turn !== 4)) { - return res.status(403).send({ message: 'Playing' }) - } + try { + updatePlayerCoins( + pokerRooms[roomId].players[userId], + pokerRooms[roomId].players[userId].bank, + pokerRooms[roomId].fakeMoney, + ); + delete pokerRooms[roomId].players[userId]; - try { - updatePlayerCoins(pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney); - delete pokerRooms[roomId].players[userId] + if (userId === pokerRooms[roomId].host_id) { + const newHostId = Object.keys(pokerRooms[roomId].players).find((id) => id !== userId); + if (!newHostId) { + delete pokerRooms[roomId]; + } else { + pokerRooms[roomId].host_id = newHostId; + } + } + } catch (e) { + console.log(e); + } - if (userId === pokerRooms[roomId].host_id) { - const newHostId = Object.keys(pokerRooms[roomId].players).find(id => id !== userId) - if (!newHostId) { - delete pokerRooms[roomId] - } else { - pokerRooms[roomId].host_id = newHostId - } - } - } catch (e) { - console.log(e) - } + await emitPokerUpdate({ type: "player-kicked" }); + return res.status(200); + }); - await emitPokerUpdate({ type: 'player-kicked' }); - return res.status(200) - }); + // --- Game Action Endpoints --- - // --- Game Action Endpoints --- + router.post("/start", async (req, res) => { + const { roomId } = req.body; + const room = pokerRooms[roomId]; + if (!room) return res.status(404).json({ message: "Room not found." }); + if (Object.keys(room.players).length < 2) return res.status(400).json({ message: "Not enough players to start." }); - router.post('/start', async (req, res) => { - const { roomId } = req.body; - const room = pokerRooms[roomId]; - if (!room) return res.status(404).json({ message: 'Room not found.' }); - if (Object.keys(room.players).length < 2) return res.status(400).json({ message: 'Not enough players to start.' }); + await startNewHand(room, io); + res.status(200).json({ message: "Game started." }); + }); - await startNewHand(room, io); - res.status(200).json({ message: 'Game started.' }); - }); + // NEW: Endpoint to start the next hand + router.post("/next-hand", async (req, res) => { + const { roomId } = req.body; + const room = pokerRooms[roomId]; + if (!room || !room.waiting_for_restart) { + return res.status(400).json({ message: "Not ready for the next hand." }); + } + await startNewHand(room, io); + res.status(200).json({ message: "Next hand started." }); + }); - // NEW: Endpoint to start the next hand - router.post('/next-hand', async (req, res) => { - const { roomId } = req.body; - const room = pokerRooms[roomId]; - if (!room || !room.waiting_for_restart) { - return res.status(400).json({ message: 'Not ready for the next hand.' }); - } - await startNewHand(room, io); - res.status(200).json({ message: 'Next hand started.' }); - }); + router.post("/action/:action", async (req, res) => { + const { playerId, amount, roomId } = req.body; + const { action } = req.params; + const room = pokerRooms[roomId]; - router.post('/action/:action', async (req, res) => { - const { playerId, amount, roomId } = req.body; - const { action } = req.params; - const room = pokerRooms[roomId]; + if (!room || !room.players[playerId] || room.current_player !== playerId) { + return res.status(403).json({ message: "It's not your turn or you are not in this game." }); + } - if (!room || !room.players[playerId] || room.current_player !== playerId) { - return res.status(403).json({ message: "It's not your turn or you are not in this game." }); - } + const player = room.players[playerId]; - const player = room.players[playerId]; + switch (action) { + case "fold": + player.folded = true; + await emitPokerToast({ + type: "player-fold", + playerId: player.id, + playerName: player.globalName, + roomId: room.id, + }); + break; + case "check": + if (player.bet < room.highest_bet) return res.status(400).json({ message: "Cannot check." }); + await emitPokerToast({ + type: "player-check", + playerId: player.id, + playerName: player.globalName, + roomId: room.id, + }); + break; + case "call": + const callAmount = Math.min(room.highest_bet - player.bet, player.bank); + player.bank -= callAmount; + player.bet += callAmount; + if (player.bank === 0) player.allin = true; + await emitPokerToast({ + type: "player-call", + playerId: player.id, + playerName: player.globalName, + roomId: room.id, + }); + break; + case "raise": + if (amount <= 0 || amount > player.bank || player.bet + amount <= room.highest_bet) { + return res.status(400).json({ message: "Invalid raise amount." }); + } + player.bank -= amount; + player.bet += amount; + if (player.bank === 0) player.allin = true; + room.highest_bet = player.bet; + await emitPokerToast({ + type: "player-raise", + amount: amount, + playerId: player.id, + playerName: player.globalName, + roomId: room.id, + }); + break; + default: + return res.status(400).json({ message: "Invalid action." }); + } - switch(action) { - case 'fold': - player.folded = true; - await emitPokerToast({ - type: 'player-fold', - playerId: player.id, - playerName: player.globalName, - roomId: room.id, - }) - break; - case 'check': - if (player.bet < room.highest_bet) return res.status(400).json({ message: 'Cannot check.' }); - await emitPokerToast({ - type: 'player-check', - playerId: player.id, - playerName: player.globalName, - roomId: room.id, - }) - break; - case 'call': - const callAmount = Math.min(room.highest_bet - player.bet, player.bank); - player.bank -= callAmount; - player.bet += callAmount; - if (player.bank === 0) player.allin = true; - await emitPokerToast({ - type: 'player-call', - playerId: player.id, - playerName: player.globalName, - roomId: room.id, - }) - break; - case 'raise': - if (amount <= 0 || amount > player.bank || (player.bet + amount) <= room.highest_bet) { - return res.status(400).json({ message: 'Invalid raise amount.' }); - } - player.bank -= amount; - player.bet += amount; - if (player.bank === 0) player.allin = true; - room.highest_bet = player.bet; - await emitPokerToast({ - type: 'player-raise', - amount: amount, - playerId: player.id, - playerName: player.globalName, - roomId: room.id, - }) - break; - default: return res.status(400).json({ message: 'Invalid action.' }); - } + player.last_played_turn = room.current_turn; + await checkRoundCompletion(room, io); + res.status(200).json({ message: `Action '${action}' successful.` }); + }); - player.last_played_turn = room.current_turn; - await checkRoundCompletion(room, io); - res.status(200).json({ message: `Action '${action}' successful.` }); - }); - - return router; + return router; } - // --- Helper Functions --- async function joinRoom(roomId, userId, io) { - const user = await client.users.fetch(userId); - const userDB = getUser.get(userId); - const room = pokerRooms[roomId]; + const user = await client.users.fetch(userId); + const userDB = getUser.get(userId); + const room = pokerRooms[roomId]; - const playerObject = { - id: userId, globalName: user.globalName || user.username, avatar: user.displayAvatarURL({ dynamic: true, size: 256 }), - hand: [], bank: room.minBet, bet: 0, folded: false, allin: false, - last_played_turn: null, solve: null - }; + const playerObject = { + id: userId, + globalName: user.globalName || user.username, + avatar: user.displayAvatarURL({ dynamic: true, size: 256 }), + hand: [], + bank: room.minBet, + bet: 0, + folded: false, + allin: false, + last_played_turn: null, + solve: null, + }; - if (room.playing) { - room.queue[userId] = playerObject; - } else { - room.players[userId] = playerObject; - if (!room.fakeMoney) { - updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet }); - insertLog.run({ - id: `${userId}-poker-${Date.now()}`, - user_id: userId, target_user_id: null, - action: 'POKER_JOIN', - coins_amount: -room.minBet, user_new_amount: userDB.coins - room.minBet, - }) - } - } + if (room.playing) { + room.queue[userId] = playerObject; + } else { + room.players[userId] = playerObject; + if (!room.fakeMoney) { + updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet }); + insertLog.run({ + id: `${userId}-poker-${Date.now()}`, + user_id: userId, + target_user_id: null, + action: "POKER_JOIN", + coins_amount: -room.minBet, + user_new_amount: userDB.coins - room.minBet, + }); + } + } - await emitPokerUpdate({ room: room, type: 'player-joined' }); + await emitPokerUpdate({ room: room, type: "player-joined" }); } async function startNewHand(room, io) { - const playerIds = Object.keys(room.players); - if (playerIds.length < 2) { - room.playing = false; // Not enough players to continue - await emitPokerUpdate({ room: room, type: 'new-hand' }); - return; - } + const playerIds = Object.keys(room.players); + if (playerIds.length < 2) { + room.playing = false; // Not enough players to continue + await emitPokerUpdate({ room: room, type: "new-hand" }); + return; + } - room.playing = true; - room.current_turn = 0; // Pre-flop - room.pioche = initialShuffledCards(); - room.tapis = []; - room.winners = []; - room.waiting_for_restart = false; - room.highest_bet = 20; - room.last_move_at = Date.now(); + room.playing = true; + room.current_turn = 0; // Pre-flop + room.pioche = initialShuffledCards(); + room.tapis = []; + room.winners = []; + room.waiting_for_restart = false; + room.highest_bet = 20; + room.last_move_at = Date.now(); - // Rotate dealer - const oldDealerIndex = playerIds.indexOf(room.dealer); - room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length]; + // Rotate dealer + const oldDealerIndex = playerIds.indexOf(room.dealer); + room.dealer = playerIds[(oldDealerIndex + 1) % playerIds.length]; - Object.values(room.players).forEach(p => { - p.hand = [room.pioche.pop(), room.pioche.pop()]; - p.bet = 0; p.folded = false; p.allin = false; p.last_played_turn = null; - }); - updatePlayerHandSolves(room); // NEW: Calculate initial hand strength + Object.values(room.players).forEach((p) => { + p.hand = [room.pioche.pop(), room.pioche.pop()]; + p.bet = 0; + p.folded = false; + p.allin = false; + p.last_played_turn = null; + }); + updatePlayerHandSolves(room); // NEW: Calculate initial hand strength - // Handle blinds based on new dealer - const dealerIndex = playerIds.indexOf(room.dealer); - const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]]; - const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]]; - room.sb = sbPlayer.id; - room.bb = bbPlayer.id; + // Handle blinds based on new dealer + const dealerIndex = playerIds.indexOf(room.dealer); + const sbPlayer = room.players[playerIds[(dealerIndex + 1) % playerIds.length]]; + const bbPlayer = room.players[playerIds[(dealerIndex + 2) % playerIds.length]]; + room.sb = sbPlayer.id; + room.bb = bbPlayer.id; - sbPlayer.bank -= 10; sbPlayer.bet = 10; - bbPlayer.bank -= 20; bbPlayer.bet = 20; + sbPlayer.bank -= 10; + sbPlayer.bet = 10; + bbPlayer.bank -= 20; + bbPlayer.bet = 20; - bbPlayer.last_played_turn = 0; - room.current_player = playerIds[(dealerIndex + 3) % playerIds.length]; - await emitPokerUpdate({ room: room, type: 'room-started' }); + bbPlayer.last_played_turn = 0; + room.current_player = playerIds[(dealerIndex + 3) % playerIds.length]; + await emitPokerUpdate({ room: room, type: "room-started" }); } async function checkRoundCompletion(room, io) { - room.last_move_at = Date.now(); - const roundResult = checkEndOfBettingRound(room); + room.last_move_at = Date.now(); + const roundResult = checkEndOfBettingRound(room); - if (roundResult.endRound) { - if (roundResult.winner) { - await handleShowdown(room, io, [roundResult.winner]); - } else { - await advanceToNextPhase(room, io, roundResult.nextPhase); - } - } else { - room.current_player = getNextActivePlayer(room); - await emitPokerUpdate({ room: room, type: 'round-continue' }); - } + if (roundResult.endRound) { + if (roundResult.winner) { + await handleShowdown(room, io, [roundResult.winner]); + } else { + await advanceToNextPhase(room, io, roundResult.nextPhase); + } + } else { + room.current_player = getNextActivePlayer(room); + await emitPokerUpdate({ room: room, type: "round-continue" }); + } } async function advanceToNextPhase(room, io, phase) { - Object.values(room.players).forEach(p => { if (!p.folded) p.last_played_turn = null; }); + Object.values(room.players).forEach((p) => { + if (!p.folded) p.last_played_turn = null; + }); - switch(phase) { - case 'flop': - room.current_turn = 1; - room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop()); - break; - case 'turn': - room.current_turn = 2; - room.tapis.push(room.pioche.pop()); - break; - case 'river': - room.current_turn = 3; - room.tapis.push(room.pioche.pop()); - break; - case 'showdown': - await handleShowdown(room, io, checkRoomWinners(room)); - return; - case 'progressive-showdown': - await emitPokerUpdate({ room: room, type: 'progressive-showdown' }); - while(room.tapis.length < 5) { - await sleep(500); - room.tapis.push(room.pioche.pop()); - updatePlayerHandSolves(room); - await emitPokerUpdate({ room: room, type: 'progressive-showdown' }); - } - await handleShowdown(room, io, checkRoomWinners(room)); - return; - } - updatePlayerHandSolves(room); // NEW: Update hand strength after new cards - room.current_player = getFirstActivePlayerAfterDealer(room); - await emitPokerUpdate({ room: room, type: 'phase-advanced' }); + switch (phase) { + case "flop": + room.current_turn = 1; + room.tapis.push(room.pioche.pop(), room.pioche.pop(), room.pioche.pop()); + break; + case "turn": + room.current_turn = 2; + room.tapis.push(room.pioche.pop()); + break; + case "river": + room.current_turn = 3; + room.tapis.push(room.pioche.pop()); + break; + case "showdown": + await handleShowdown(room, io, checkRoomWinners(room)); + return; + case "progressive-showdown": + await emitPokerUpdate({ room: room, type: "progressive-showdown" }); + while (room.tapis.length < 5) { + await sleep(500); + room.tapis.push(room.pioche.pop()); + updatePlayerHandSolves(room); + await emitPokerUpdate({ room: room, type: "progressive-showdown" }); + } + await handleShowdown(room, io, checkRoomWinners(room)); + return; + } + updatePlayerHandSolves(room); // NEW: Update hand strength after new cards + room.current_player = getFirstActivePlayerAfterDealer(room); + await emitPokerUpdate({ room: room, type: "phase-advanced" }); } async function handleShowdown(room, io, winners) { - room.current_turn = 4; - room.playing = false; - room.waiting_for_restart = true; - room.winners = winners; - room.current_player = null; + room.current_turn = 4; + room.playing = false; + room.waiting_for_restart = true; + room.winners = winners; + room.current_player = null; - let totalPot = 0; - Object.values(room.players).forEach(p => { totalPot += p.bet; }); + let totalPot = 0; + Object.values(room.players).forEach((p) => { + totalPot += p.bet; + }); - const winAmount = winners.length > 0 ? Math.floor(totalPot / winners.length) : 0; + const winAmount = winners.length > 0 ? Math.floor(totalPot / winners.length) : 0; - winners.forEach(winnerId => { - const winnerPlayer = room.players[winnerId]; - if(winnerPlayer) { - winnerPlayer.bank += winAmount; - } - }); + winners.forEach((winnerId) => { + const winnerPlayer = room.players[winnerId]; + if (winnerPlayer) { + winnerPlayer.bank += winAmount; + } + }); - await clearAfkPlayers(room); + await clearAfkPlayers(room); - //await pokerEloHandler(room); - await emitPokerUpdate({ room: room, type: 'showdown' }); - await emitPokerToast({ - type: 'player-winner', - playerIds: winners, - roomId: room.id, - amount: winAmount, - }) + //await pokerEloHandler(room); + await emitPokerUpdate({ room: room, type: "showdown" }); + await emitPokerToast({ + type: "player-winner", + playerIds: winners, + roomId: room.id, + amount: winAmount, + }); } // NEW: Function to calculate and update hand strength for all players function updatePlayerHandSolves(room) { - const communityCards = room.tapis; - for (const player of Object.values(room.players)) { - if (!player.folded) { - const allCards = [...communityCards, ...player.hand]; - player.solve = Hand.solve(allCards).descr; - } - } + const communityCards = room.tapis; + for (const player of Object.values(room.players)) { + if (!player.folded) { + const allCards = [...communityCards, ...player.hand]; + player.solve = Hand.solve(allCards).descr; + } + } } function updatePlayerCoins(player, amount, isFake) { - if (isFake) return; - const user = getUser.get(player.id); - if (!user) return; + if (isFake) return; + const user = getUser.get(player.id); + if (!user) return; - const userDB = getUser.get(player.id); - updateUserCoins.run({ id: player.id, coins: userDB.coins + amount }); - insertLog.run({ - id: `${player.id}-poker-${Date.now()}`, - user_id: player.id, target_user_id: null, - action: `POKER_${amount > 0 ? 'WIN' : 'LOSE'}`, - coins_amount: amount, user_new_amount: userDB.coins + amount, - }); + const userDB = getUser.get(player.id); + updateUserCoins.run({ id: player.id, coins: userDB.coins + amount }); + insertLog.run({ + id: `${player.id}-poker-${Date.now()}`, + user_id: player.id, + target_user_id: null, + action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`, + coins_amount: amount, + user_new_amount: userDB.coins + amount, + }); } async function clearAfkPlayers(room) { - Object.keys(room.afk).forEach(playerId => { - if (room.players[playerId]) { - updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney); - delete room.players[playerId]; - } - }); - room.afk = {}; -} \ No newline at end of file + Object.keys(room.afk).forEach((playerId) => { + if (room.players[playerId]) { + updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney); + delete room.players[playerId]; + } + }); + room.afk = {}; +} diff --git a/src/server/routes/solitaire.js b/src/server/routes/solitaire.js index c1883ce..3d5abb5 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -1,18 +1,35 @@ -import express from 'express'; +import express from "express"; // --- Game Logic Imports --- import { - createDeck, shuffle, deal, isValidMove, moveCard, drawCard, - checkWinCondition, createSeededRNG, seededShuffle, undoMove, draw3Cards, checkAutoSolve, autoSolveMoves -} from '../../game/solitaire.js'; + createDeck, + shuffle, + deal, + isValidMove, + moveCard, + drawCard, + checkWinCondition, + createSeededRNG, + seededShuffle, + undoMove, + draw3Cards, + checkAutoSolve, + autoSolveMoves, +} from "../../game/solitaire.js"; // --- Game State & Database Imports --- -import { activeSolitaireGames } from '../../game/state.js'; +import { activeSolitaireGames } from "../../game/state.js"; import { - getSOTD, getUser, insertSOTDStats, deleteUserSOTDStats, - getUserSOTDStats, updateUserCoins, insertLog, getAllSOTDStats -} from '../../database/index.js'; -import {socketEmit} from "../socket.js"; + getSOTD, + getUser, + insertSOTDStats, + deleteUserSOTDStats, + getUserSOTDStats, + updateUserCoins, + insertLog, + getAllSOTDStats, +} from "../../database/index.js"; +import { socketEmit } from "../socket.js"; // Create a new router instance const router = express.Router(); @@ -24,248 +41,262 @@ const router = express.Router(); * @returns {object} The configured Express router. */ export function solitaireRoutes(client, io) { + // --- Game Initialization Endpoints --- - // --- Game Initialization Endpoints --- + router.post("/start", (req, res) => { + const { userId, userSeed, hardMode } = req.body; + if (!userId) return res.status(400).json({ error: "User ID is required." }); - router.post('/start', (req, res) => { - const { userId, userSeed, hardMode } = req.body; - if (!userId) return res.status(400).json({ error: 'User ID is required.' }); + // If a game already exists for the user, return it instead of creating a new one. + if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) { + return res.json({ + success: true, + gameState: activeSolitaireGames[userId], + }); + } - // If a game already exists for the user, return it instead of creating a new one. - if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) { - return res.json({ success: true, gameState: activeSolitaireGames[userId] }); - } + let deck, seed; + if (userSeed) { + // Use the provided seed to create a deterministic game + seed = userSeed; + } else { + // Create a random seed if none is provided + seed = Date.now().toString(36) + Math.random().toString(36).substr(2); + } - let deck, seed; - if (userSeed) { - // Use the provided seed to create a deterministic game - seed = userSeed; - } else { - // Create a random seed if none is provided - seed = Date.now().toString(36) + Math.random().toString(36).substr(2); - } + let numericSeed = 0; + for (let i = 0; i < seed.length; i++) { + numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xffffffff; + } - let numericSeed = 0; - for (let i = 0; i < seed.length; i++) { - numericSeed = (numericSeed + seed.charCodeAt(i)) & 0xFFFFFFFF; - } + const rng = createSeededRNG(numericSeed); + deck = seededShuffle(createDeck(), rng); - const rng = createSeededRNG(numericSeed); - deck = seededShuffle(createDeck(), rng); + const gameState = deal(deck); + gameState.seed = seed; + gameState.isSOTD = false; + gameState.score = 0; + gameState.moves = 0; + gameState.hist = []; + gameState.hardMode = hardMode ?? false; + gameState.autocompleting = false; + activeSolitaireGames[userId] = gameState; - const gameState = deal(deck); - gameState.seed = seed; - gameState.isSOTD = false; - gameState.score = 0; - gameState.moves = 0; - gameState.hist = []; - gameState.hardMode = hardMode ?? false; - gameState.autocompleting = false; - activeSolitaireGames[userId] = gameState; + res.json({ success: true, gameState }); + }); - res.json({ success: true, gameState }); - }); - - router.post('/start/sotd', (req, res) => { - const { userId } = req.body; - /*if (!userId || !getUser.get(userId)) { + router.post("/start/sotd", (req, res) => { + const { userId } = req.body; + /*if (!userId || !getUser.get(userId)) { return res.status(404).json({ error: 'User not found.' }); }*/ - if (activeSolitaireGames[userId]?.isSOTD) { - return res.json({ success: true, gameState: activeSolitaireGames[userId] }); - } + if (activeSolitaireGames[userId]?.isSOTD) { + return res.json({ + success: true, + gameState: activeSolitaireGames[userId], + }); + } - const sotd = getSOTD.get(); - if (!sotd) { - return res.status(500).json({ error: 'Solitaire of the Day is not configured.'}); - } + const sotd = getSOTD.get(); + if (!sotd) { + return res.status(500).json({ error: "Solitaire of the Day is not configured." }); + } - const gameState = { - tableauPiles: JSON.parse(sotd.tableauPiles), - foundationPiles: JSON.parse(sotd.foundationPiles), - stockPile: JSON.parse(sotd.stockPile), - wastePile: JSON.parse(sotd.wastePile), - isDone: false, - isSOTD: true, - startTime: Date.now(), - endTime: null, - moves: 0, - score: 0, - seed: sotd.seed, - hist: [], - hardMode: false, - autocompleting: false, - }; + const gameState = { + tableauPiles: JSON.parse(sotd.tableauPiles), + foundationPiles: JSON.parse(sotd.foundationPiles), + stockPile: JSON.parse(sotd.stockPile), + wastePile: JSON.parse(sotd.wastePile), + isDone: false, + isSOTD: true, + startTime: Date.now(), + endTime: null, + moves: 0, + score: 0, + seed: sotd.seed, + hist: [], + hardMode: false, + autocompleting: false, + }; - activeSolitaireGames[userId] = gameState; - res.json({ success: true, gameState }); - }); + activeSolitaireGames[userId] = gameState; + res.json({ success: true, gameState }); + }); - // --- Game State & Action Endpoints --- + // --- Game State & Action Endpoints --- - router.get('/sotd/rankings', (req, res) => { - try { - const rankings = getAllSOTDStats.all(); - res.json({ rankings }); - } catch(e) { - res.status(500).json({ error: "Failed to fetch SOTD rankings."}); - } - }); + router.get("/sotd/rankings", (req, res) => { + try { + const rankings = getAllSOTDStats.all(); + res.json({ rankings }); + } catch (e) { + res.status(500).json({ error: "Failed to fetch SOTD rankings." }); + } + }); - router.get('/state/:userId', (req, res) => { - const { userId } = req.params; - const gameState = activeSolitaireGames[userId]; - if (gameState) { - res.json({ success: true, gameState }); - } else { - res.status(404).json({ error: 'No active game found for this user.' }); - } - }); + router.get("/state/:userId", (req, res) => { + const { userId } = req.params; + const gameState = activeSolitaireGames[userId]; + if (gameState) { + res.json({ success: true, gameState }); + } else { + res.status(404).json({ error: "No active game found for this user." }); + } + }); - router.post('/reset', (req, res) => { - const { userId } = req.body; - if (activeSolitaireGames[userId]) { - delete activeSolitaireGames[userId]; - } - res.json({ success: true, message: "Game reset."}); - }); + router.post("/reset", (req, res) => { + const { userId } = req.body; + if (activeSolitaireGames[userId]) { + delete activeSolitaireGames[userId]; + } + res.json({ success: true, message: "Game reset." }); + }); - router.post('/move', async (req, res) => { - const { userId, ...moveData } = req.body; - const gameState = activeSolitaireGames[userId]; + router.post("/move", async (req, res) => { + const { userId, ...moveData } = req.body; + const gameState = activeSolitaireGames[userId]; - if (!gameState) return res.status(404).json({ error: 'Game not found.' }); - if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'}); + if (!gameState) return res.status(404).json({ error: "Game not found." }); + if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." }); - if (isValidMove(gameState, moveData)) { - moveCard(gameState, moveData); - updateGameStats(gameState, 'move', moveData); + if (isValidMove(gameState, moveData)) { + moveCard(gameState, moveData); + updateGameStats(gameState, "move", moveData); - if (!gameState.autocompleting) { - const canAutoSolve = checkAutoSolve(gameState); - if (canAutoSolve) { - gameState.autocompleting = true; - autoSolveMoves(userId, gameState) - } - } + if (!gameState.autocompleting) { + const canAutoSolve = checkAutoSolve(gameState); + if (canAutoSolve) { + gameState.autocompleting = true; + autoSolveMoves(userId, gameState); + } + } - const win = checkWinCondition(gameState); - if (win) { - gameState.isDone = true; - await handleWin(userId, gameState, io); - } - res.json({ success: true, gameState, win }); - } else { - res.status(400).json({ error: 'Invalid move' }); - } - }); + const win = checkWinCondition(gameState); + if (win) { + gameState.isDone = true; + await handleWin(userId, gameState, io); + } + res.json({ success: true, gameState, win }); + } else { + res.status(400).json({ error: "Invalid move" }); + } + }); - router.post('/draw', (req, res) => { - const { userId } = req.body; - const gameState = activeSolitaireGames[userId]; + router.post("/draw", (req, res) => { + const { userId } = req.body; + const gameState = activeSolitaireGames[userId]; - if (!gameState) return res.status(404).json({ error: 'Game not found.' }); - if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'}); + if (!gameState) return res.status(404).json({ error: "Game not found." }); + if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." }); - if (gameState.hardMode) { - draw3Cards(gameState); - } else { - drawCard(gameState); - } - updateGameStats(gameState, 'draw'); - res.json({ success: true, gameState }); - }); + if (gameState.hardMode) { + draw3Cards(gameState); + } else { + drawCard(gameState); + } + updateGameStats(gameState, "draw"); + res.json({ success: true, gameState }); + }); - router.post('/undo', (req, res) => { - const { userId } = req.body; - const gameState = activeSolitaireGames[userId]; + router.post("/undo", (req, res) => { + const { userId } = req.body; + const gameState = activeSolitaireGames[userId]; - if (!gameState) return res.status(404).json({ error: 'Game not found.' }); - if (gameState.isDone) return res.status(400).json({ error: 'This game is already completed.'}); - if (gameState.hist.length === 0) return res.status(400).json({ error: 'No moves to undo.'}); + if (!gameState) return res.status(404).json({ error: "Game not found." }); + if (gameState.isDone) return res.status(400).json({ error: "This game is already completed." }); + if (gameState.hist.length === 0) return res.status(400).json({ error: "No moves to undo." }); - undoMove(gameState); - res.json({ success: true, gameState }); - }) + undoMove(gameState); + res.json({ success: true, gameState }); + }); - return router; + return router; } - // --- Helper Functions --- /** Updates game stats like moves and score after an action. */ function updateGameStats(gameState, actionType, moveData = {}) { - // if (!gameState.isSOTD) return; // Only track stats for SOTD + // if (!gameState.isSOTD) return; // Only track stats for SOTD - gameState.moves++; - if (actionType === 'move') { - if (moveData.destPileType === 'foundationPiles') { - gameState.score += 10; // Move card to foundation - } - if (moveData.sourcePileType === 'foundationPiles') { - gameState.score -= 15; // Move card from foundation (penalty) - } - } - if(actionType === 'draw' && gameState.wastePile.length === 0) { - // Penalty for cycling through an empty stock pile - gameState.score -= 5; - } + gameState.moves++; + if (actionType === "move") { + if (moveData.destPileType === "foundationPiles") { + gameState.score += 10; // Move card to foundation + } + if (moveData.sourcePileType === "foundationPiles") { + gameState.score -= 15; // Move card from foundation (penalty) + } + } + if (actionType === "draw" && gameState.wastePile.length === 0) { + // Penalty for cycling through an empty stock pile + gameState.score -= 5; + } } /** Handles the logic when a game is won. */ async function handleWin(userId, gameState, io) { - const currentUser = getUser.get(userId); - if (!currentUser) return; + const currentUser = getUser.get(userId); + if (!currentUser) return; - if (gameState.hardMode) { - const bonus = 100; - const newCoins = currentUser.coins + bonus; - updateUserCoins.run({ id: userId, coins: newCoins }); - insertLog.run({ - id: `${userId}-hardmode-solitaire-${Date.now()}`, user_id: userId, - action: 'HARDMODE_SOLITAIRE_WIN', target_user_id: null, - coins_amount: bonus, user_new_amount: newCoins, - }); - await socketEmit('data-updated', { table: 'users' }); - } + if (gameState.hardMode) { + const bonus = 100; + const newCoins = currentUser.coins + bonus; + updateUserCoins.run({ id: userId, coins: newCoins }); + insertLog.run({ + id: `${userId}-hardmode-solitaire-${Date.now()}`, + user_id: userId, + action: "HARDMODE_SOLITAIRE_WIN", + target_user_id: null, + coins_amount: bonus, + user_new_amount: newCoins, + }); + await socketEmit("data-updated", { table: "users" }); + } - if (!gameState.isSOTD) return; // Only process SOTD wins here + if (!gameState.isSOTD) return; // Only process SOTD wins here - gameState.endTime = Date.now(); - const timeTaken = gameState.endTime - gameState.startTime; + gameState.endTime = Date.now(); + const timeTaken = gameState.endTime - gameState.startTime; - const existingStats = getUserSOTDStats.get(userId); + const existingStats = getUserSOTDStats.get(userId); - if (!existingStats) { - // First time completing the SOTD, grant bonus coins - const bonus = 1000; - const newCoins = currentUser.coins + bonus; - updateUserCoins.run({ id: userId, coins: newCoins }); - insertLog.run({ - id: `${userId}-sotd-complete-${Date.now()}`, user_id: userId, - action: 'SOTD_WIN', target_user_id: null, - coins_amount: bonus, user_new_amount: newCoins, - }); - await socketEmit('data-updated', { table: 'users' }); - } + if (!existingStats) { + // First time completing the SOTD, grant bonus coins + const bonus = 1000; + const newCoins = currentUser.coins + bonus; + updateUserCoins.run({ id: userId, coins: newCoins }); + insertLog.run({ + id: `${userId}-sotd-complete-${Date.now()}`, + user_id: userId, + action: "SOTD_WIN", + target_user_id: null, + coins_amount: bonus, + user_new_amount: newCoins, + }); + await socketEmit("data-updated", { table: "users" }); + } - // Save the score if it's better than the previous one - const isNewBest = !existingStats || - gameState.score > existingStats.score || - (gameState.score === existingStats.score && gameState.moves < existingStats.moves) || - (gameState.score === existingStats.score && gameState.moves === existingStats.moves && timeTaken < existingStats.time); + // Save the score if it's better than the previous one + const isNewBest = + !existingStats || + gameState.score > existingStats.score || + (gameState.score === existingStats.score && gameState.moves < existingStats.moves) || + (gameState.score === existingStats.score && + gameState.moves === existingStats.moves && + timeTaken < existingStats.time); - if (isNewBest) { - deleteUserSOTDStats.run(userId) - insertSOTDStats.run({ - id: userId, user_id: userId, - time: timeTaken, - moves: gameState.moves, - score: gameState.score, - }); - await socketEmit('sotd-update') - console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`); - } -} \ No newline at end of file + if (isNewBest) { + deleteUserSOTDStats.run(userId); + insertSOTDStats.run({ + id: userId, + user_id: userId, + time: timeTaken, + moves: gameState.moves, + score: gameState.score, + }); + await socketEmit("sotd-update"); + console.log(`New SOTD high score for ${currentUser.globalName}: ${gameState.score} points.`); + } +} diff --git a/src/server/socket.js b/src/server/socket.js index fc3a28b..edc6f2d 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -1,13 +1,20 @@ -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; +import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; import { - activeTicTacToeGames, - tictactoeQueue, - activeConnect4Games, - connect4Queue, - queueMessagesEndpoints, activePredis -} from '../game/state.js'; -import { createConnect4Board, formatConnect4BoardForDiscord, checkConnect4Win, checkConnect4Draw, C4_ROWS } from '../game/various.js'; -import { eloHandler } from '../game/elo.js'; + activeTicTacToeGames, + tictactoeQueue, + activeConnect4Games, + connect4Queue, + queueMessagesEndpoints, + activePredis, +} from "../game/state.js"; +import { + createConnect4Board, + formatConnect4BoardForDiscord, + checkConnect4Win, + checkConnect4Draw, + C4_ROWS, +} from "../game/various.js"; +import { eloHandler } from "../game/elo.js"; import { getUser } from "../database/index.js"; // --- Module-level State --- @@ -16,70 +23,73 @@ let io; // --- Main Initialization Function --- export function initializeSocket(server, client) { - io = server; + io = server; - io.on('connection', (socket) => { - socket.on('user-connected', async (userId) => { - if (!userId) return; - await refreshQueuesForUser(userId, client); - }); + io.on("connection", (socket) => { + socket.on("user-connected", async (userId) => { + if (!userId) return; + await refreshQueuesForUser(userId, client); + }); - registerTicTacToeEvents(socket, client); - registerConnect4Events(socket, client); + registerTicTacToeEvents(socket, client); + registerConnect4Events(socket, client); - socket.on('tictactoe:queue:leave', async ({ discordId }) => await refreshQueuesForUser(discordId, client)); + socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client)); - // catch tab kills / network drops - socket.on('disconnecting', async () => { - const discordId = socket.handshake.auth?.discordId; // or your mapping - await refreshQueuesForUser(discordId, client); - }); + // catch tab kills / network drops + socket.on("disconnecting", async () => { + const discordId = socket.handshake.auth?.discordId; // or your mapping + await refreshQueuesForUser(discordId, client); + }); - socket.on('disconnect', () => { - // - }); - }); + socket.on("disconnect", () => { + // + }); + }); - setInterval(cleanupStaleGames, 5 * 60 * 1000); + setInterval(cleanupStaleGames, 5 * 60 * 1000); } export function getSocketIo() { - return io; + return io; } // --- Event Registration --- function registerTicTacToeEvents(socket, client) { - socket.on('tictactoeconnection', (e) => refreshQueuesForUser(e.id, client)); - socket.on('tictactoequeue', (e) => onQueueJoin(client, 'tictactoe', e.playerId)); - socket.on('tictactoeplaying', (e) => onTicTacToeMove(client, e)); - socket.on('tictactoegameOver', (e) => onGameOver(client, 'tictactoe', e.playerId, e.winner)); + socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client)); + socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId)); + socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e)); + socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner)); } function registerConnect4Events(socket, client) { - socket.on('connect4connection', (e) => refreshQueuesForUser(e.id, client)); - socket.on('connect4queue', (e) => onQueueJoin(client, 'connect4', e.playerId)); - socket.on('connect4playing', (e) => onConnect4Move(client, e)); - socket.on('connect4NoTime', (e) => onGameOver(client, 'connect4', e.playerId, e.winner, '(temps écoulé)')); + socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client)); + socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId)); + socket.on("connect4playing", (e) => onConnect4Move(client, e)); + socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)")); } // --- Core Handlers (Preserving Original Logic) --- async function onQueueJoin(client, gameType, playerId) { - if (!playerId) return; - const { queue, activeGames, title, url } = getGameAssets(gameType); + if (!playerId) return; + const { queue, activeGames, title, url } = getGameAssets(gameType); - if (queue.includes(playerId) || Object.values(activeGames).some(g => g.p1.id === playerId || g.p2.id === playerId)) { - return; - } + if ( + queue.includes(playerId) || + Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId) + ) { + return; + } - queue.push(playerId); - console.log(`[${title}] Player ${playerId} joined the queue.`); + queue.push(playerId); + console.log(`[${title}] Player ${playerId} joined the queue.`); - if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url); - if (queue.length >= 2) await createGame(client, gameType); + if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url); + if (queue.length >= 2) await createGame(client, gameType); - await emitQueueUpdate(client, gameType); + await emitQueueUpdate(client, gameType); } /** @@ -88,262 +98,365 @@ async function onQueueJoin(client, gameType, playerId) { * @returns {boolean} - True if the player has won, false otherwise. */ function checkTicTacToeWin(moves) { - const winningCombinations = [ - [1, 2, 3], [4, 5, 6], [7, 8, 9], // Rows - [1, 4, 7], [2, 5, 8], [3, 6, 9], // Columns - [1, 5, 9], [3, 5, 7] // Diagonals - ]; - for (const combination of winningCombinations) { - if (combination.every(num => moves.includes(num))) { - return true; - } - } - return false; + const winningCombinations = [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], // Rows + [1, 4, 7], + [2, 5, 8], + [3, 6, 9], // Columns + [1, 5, 9], + [3, 5, 7], // Diagonals + ]; + for (const combination of winningCombinations) { + if (combination.every((num) => moves.includes(num))) { + return true; + } + } + return false; } async function onTicTacToeMove(client, eventData) { - const { playerId, value, boxId } = eventData; - const lobby = Object.values(activeTicTacToeGames).find(g => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver); - if (!lobby) return; + const { playerId, value, boxId } = eventData; + const lobby = Object.values(activeTicTacToeGames).find( + (g) => (g.p1.id === playerId || g.p2.id === playerId) && !g.gameOver, + ); + if (!lobby) return; - const isP1Turn = lobby.sum % 2 === 1 && value === 'X' && lobby.p1.id === playerId; - const isP2Turn = lobby.sum % 2 === 0 && value === 'O' && lobby.p2.id === playerId; + const isP1Turn = lobby.sum % 2 === 1 && value === "X" && lobby.p1.id === playerId; + const isP2Turn = lobby.sum % 2 === 0 && value === "O" && lobby.p2.id === playerId; - if (isP1Turn || isP2Turn) { - const playerMoves = isP1Turn ? lobby.xs : lobby.os; - playerMoves.push(boxId); - lobby.sum++; - lobby.lastmove = Date.now(); + if (isP1Turn || isP2Turn) { + const playerMoves = isP1Turn ? lobby.xs : lobby.os; + playerMoves.push(boxId); + lobby.sum++; + lobby.lastmove = Date.now(); - if (isP1Turn) lobby.p1.move = boxId - if (isP2Turn) lobby.p2.move = boxId + if (isP1Turn) lobby.p1.move = boxId; + if (isP2Turn) lobby.p2.move = boxId; - io.emit('tictactoeplaying', { allPlayers: Object.values(activeTicTacToeGames) }); - const hasWon = checkTicTacToeWin(playerMoves); - if (hasWon) { - // The current player has won. End the game. - await onGameOver(client, 'tictactoe', playerId, playerId); - } else if (lobby.sum > 9) { - // It's a draw (9 moves made, sum is now 10). End the game. - await onGameOver(client, 'tictactoe', playerId, null); // null winner for a draw - } else { - // The game continues. Update the state and notify clients. - await updateDiscordMessage(client, lobby, 'Tic Tac Toe'); - } - } - await emitQueueUpdate(client, 'tictactoe'); + io.emit("tictactoeplaying", { + allPlayers: Object.values(activeTicTacToeGames), + }); + const hasWon = checkTicTacToeWin(playerMoves); + if (hasWon) { + // The current player has won. End the game. + await onGameOver(client, "tictactoe", playerId, playerId); + } else if (lobby.sum > 9) { + // It's a draw (9 moves made, sum is now 10). End the game. + await onGameOver(client, "tictactoe", playerId, null); // null winner for a draw + } else { + // The game continues. Update the state and notify clients. + await updateDiscordMessage(client, lobby, "Tic Tac Toe"); + } + } + await emitQueueUpdate(client, "tictactoe"); } async function onConnect4Move(client, eventData) { - const { playerId, col } = eventData; - const lobby = Object.values(activeConnect4Games).find(l => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver); - if (!lobby || lobby.turn !== playerId) return; + const { playerId, col } = eventData; + const lobby = Object.values(activeConnect4Games).find( + (l) => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver, + ); + if (!lobby || lobby.turn !== playerId) return; - const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2; - let row; - for (row = C4_ROWS - 1; row >= 0; row--) { - if (lobby.board[row][col] === null) { - lobby.board[row][col] = player.val; - break; - } - } - if (row < 0) return; + const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2; + let row; + for (row = C4_ROWS - 1; row >= 0; row--) { + if (lobby.board[row][col] === null) { + lobby.board[row][col] = player.val; + break; + } + } + if (row < 0) return; - lobby.lastmove = Date.now(); - const winCheck = checkConnect4Win(lobby.board, player.val); + lobby.lastmove = Date.now(); + const winCheck = checkConnect4Win(lobby.board, player.val); - let winnerId = null; - if (winCheck.win) { - lobby.winningPieces = winCheck.pieces; - winnerId = player.id; - } else if (checkConnect4Draw(lobby.board)) { - winnerId = null; // Represents a draw - } else { - lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id; - io.emit('connect4playing', { allPlayers: Object.values(activeConnect4Games) }); - await emitQueueUpdate(client, 'connact4'); - await updateDiscordMessage(client, lobby, 'Puissance 4'); - return; - } - await onGameOver(client, 'connect4', playerId, winnerId); + let winnerId = null; + if (winCheck.win) { + lobby.winningPieces = winCheck.pieces; + winnerId = player.id; + } else if (checkConnect4Draw(lobby.board)) { + winnerId = null; // Represents a draw + } else { + lobby.turn = lobby.p1.id === playerId ? lobby.p2.id : lobby.p1.id; + io.emit("connect4playing", { + allPlayers: Object.values(activeConnect4Games), + }); + await emitQueueUpdate(client, "connact4"); + await updateDiscordMessage(client, lobby, "Puissance 4"); + return; + } + await onGameOver(client, "connect4", playerId, winnerId); } -async function onGameOver(client, gameType, playerId, winnerId, reason = '') { - const { activeGames, title } = getGameAssets(gameType); - const gameKey = Object.keys(activeGames).find(key => key.includes(playerId)); - const game = gameKey ? activeGames[gameKey] : undefined; - if (!game || game.gameOver) return; +async function onGameOver(client, gameType, playerId, winnerId, reason = "") { + const { activeGames, title } = getGameAssets(gameType); + const gameKey = Object.keys(activeGames).find((key) => key.includes(playerId)); + const game = gameKey ? activeGames[gameKey] : undefined; + if (!game || game.gameOver) return; - game.gameOver = true; - let resultText; - if (winnerId === null) { - await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase()); - resultText = 'Égalité'; - } else { - await eloHandler(game.p1.id, game.p2.id, game.p1.id === winnerId ? 1 : 0, game.p2.id === winnerId ? 1 : 0, title.toUpperCase()); - const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name; - resultText = `Victoire de ${winnerName}`; - } + game.gameOver = true; + let resultText; + if (winnerId === null) { + await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase()); + resultText = "Égalité"; + } else { + await eloHandler( + game.p1.id, + game.p2.id, + game.p1.id === winnerId ? 1 : 0, + game.p2.id === winnerId ? 1 : 0, + title.toUpperCase(), + ); + const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name; + resultText = `Victoire de ${winnerName}`; + } - await updateDiscordMessage(client, game, title, `${resultText} ${reason}`); + await updateDiscordMessage(client, game, title, `${resultText} ${reason}`); - if(gameType === 'tictactoe') io.emit('tictactoegameOver', { game, winner: winnerId }); - if(gameType === 'connect4') io.emit('connect4gameOver', { game, winner: winnerId }); + if (gameType === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId }); + if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId }); - if (gameKey) { - setTimeout(() => delete activeGames[gameKey], 1000) - } + if (gameKey) { + setTimeout(() => delete activeGames[gameKey], 1000); + } } // --- Game Lifecycle & Discord Helpers --- async function createGame(client, gameType) { - const { queue, activeGames, title } = getGameAssets(gameType); - const p1Id = queue.shift(); - const p2Id = queue.shift(); - const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]); + const { queue, activeGames, title } = getGameAssets(gameType); + const p1Id = queue.shift(); + const p2Id = queue.shift(); + const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]); - let lobby; - if (gameType === 'tictactoe') { - lobby = { p1: { id: p1Id, name: p1.globalName, val: 'X', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'O', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, sum: 1, xs: [], os: [], gameOver: false, lastmove: Date.now() }; - } else { // connect4 - lobby = { p1: { id: p1Id, name: p1.globalName, val: 'R', avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }) }, p2: { id: p2Id, name: p2.globalName, val: 'Y', avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }) }, turn: p1Id, board: createConnect4Board(), gameOver: false, lastmove: Date.now(), winningPieces: [] }; - } + let lobby; + if (gameType === "tictactoe") { + lobby = { + p1: { + id: p1Id, + name: p1.globalName, + val: "X", + avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }), + }, + p2: { + id: p2Id, + name: p2.globalName, + val: "O", + avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }), + }, + sum: 1, + xs: [], + os: [], + gameOver: false, + lastmove: Date.now(), + }; + } else { + // connect4 + lobby = { + p1: { + id: p1Id, + name: p1.globalName, + val: "R", + avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }), + }, + p2: { + id: p2Id, + name: p2.globalName, + val: "Y", + avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }), + }, + turn: p1Id, + board: createConnect4Board(), + gameOver: false, + lastmove: Date.now(), + winningPieces: [], + }; + } - const msgId = await updateDiscordMessage(client, lobby, title); - lobby.msgId = msgId; + const msgId = await updateDiscordMessage(client, lobby, title); + lobby.msgId = msgId; - const gameKey = `${p1Id}-${p2Id}`; - activeGames[gameKey] = lobby; + const gameKey = `${p1Id}-${p2Id}`; + activeGames[gameKey] = lobby; - io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) }); - await emitQueueUpdate(client, gameType); + io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) }); + await emitQueueUpdate(client, gameType); } // --- Utility Functions --- async function refreshQueuesForUser(userId, client) { - // FIX: Mutate the array instead of reassigning it. - let index = tictactoeQueue.indexOf(userId); - if (index > -1) { - tictactoeQueue.splice(index, 1); - try { - const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); - const user = await client.users.fetch(userId); - const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]) - const updatedEmbed = new EmbedBuilder().setTitle('Tic Tac Toe').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date()); - await queueMsg.edit({ embeds: [updatedEmbed], components: [] }); - delete queueMessagesEndpoints[userId]; - } catch (e) { - console.error('Error updating queue message : ', e); - } - } + // FIX: Mutate the array instead of reassigning it. + let index = tictactoeQueue.indexOf(userId); + if (index > -1) { + tictactoeQueue.splice(index, 1); + try { + const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); + const user = await client.users.fetch(userId); + const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]); + const updatedEmbed = new EmbedBuilder() + .setTitle("Tic Tac Toe") + .setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`) + .setColor(0xed4245) + .setTimestamp(new Date()); + await queueMsg.edit({ embeds: [updatedEmbed], components: [] }); + delete queueMessagesEndpoints[userId]; + } catch (e) { + console.error("Error updating queue message : ", e); + } + } - index = connect4Queue.indexOf(userId); - if (index > -1) { - connect4Queue.splice(index, 1); - try { - const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); - const user = await client.users.fetch(userId); - const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]) - const updatedEmbed = new EmbedBuilder().setTitle('Puissance 4').setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`).setColor(0xED4245).setTimestamp(new Date()); - await queueMsg.edit({ embeds: [updatedEmbed], components: [] }); - delete queueMessagesEndpoints[userId]; - } catch (e) { - console.error('Error updating queue message : ', e); - } - } + index = connect4Queue.indexOf(userId); + if (index > -1) { + connect4Queue.splice(index, 1); + try { + const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); + const user = await client.users.fetch(userId); + const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]); + const updatedEmbed = new EmbedBuilder() + .setTitle("Puissance 4") + .setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`) + .setColor(0xed4245) + .setTimestamp(new Date()); + await queueMsg.edit({ embeds: [updatedEmbed], components: [] }); + delete queueMessagesEndpoints[userId]; + } catch (e) { + console.error("Error updating queue message : ", e); + } + } - await emitQueueUpdate(client, 'tictactoe'); - await emitQueueUpdate(client, 'connect4'); + await emitQueueUpdate(client, "tictactoe"); + await emitQueueUpdate(client, "connect4"); } async function emitQueueUpdate(client, gameType) { - const { queue, activeGames } = getGameAssets(gameType); - const names = await Promise.all(queue.map(async (id) => { - const user = await client.users.fetch(id).catch(() => null); - return user?.globalName || user?.username; - })); - io.emit(`${gameType}queue`, { allPlayers: Object.values(activeGames), queue: names.filter(Boolean) }); + const { queue, activeGames } = getGameAssets(gameType); + const names = await Promise.all( + queue.map(async (id) => { + const user = await client.users.fetch(id).catch(() => null); + return user?.globalName || user?.username; + }), + ); + io.emit(`${gameType}queue`, { + allPlayers: Object.values(activeGames), + queue: names.filter(Boolean), + }); } function getGameAssets(gameType) { - if (gameType === 'tictactoe') return { queue: tictactoeQueue, activeGames: activeTicTacToeGames, title: 'Tic Tac Toe', url: '/tic-tac-toe' }; - if (gameType === 'connect4') return { queue: connect4Queue, activeGames: activeConnect4Games, title: 'Puissance 4', url: '/connect-4' }; - return { queue: [], activeGames: {} }; + if (gameType === "tictactoe") + return { + queue: tictactoeQueue, + activeGames: activeTicTacToeGames, + title: "Tic Tac Toe", + url: "/tic-tac-toe", + }; + if (gameType === "connect4") + return { + queue: connect4Queue, + activeGames: activeConnect4Games, + title: "Puissance 4", + url: "/connect-4", + }; + return { queue: [], activeGames: {} }; } async function postQueueToDiscord(client, playerId, title, url) { - try { - const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); - const user = await client.users.fetch(playerId); - const embed = new EmbedBuilder().setTitle(title).setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`).setColor('#5865F2').setTimestamp(new Date()); - const row = new ActionRowBuilder().addComponents(new ButtonBuilder().setLabel(`Jouer contre ${user.username}`).setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`).setStyle(ButtonStyle.Link)); - const msg = await generalChannel.send({ embeds: [embed], components: [row] }); - queueMessagesEndpoints[playerId] = msg.id - } catch (e) { console.error(`Failed to post queue message for ${title}:`, e); } + try { + const generalChannel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID); + const user = await client.users.fetch(playerId); + const embed = new EmbedBuilder() + .setTitle(title) + .setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`) + .setColor("#5865F2") + .setTimestamp(new Date()); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel(`Jouer contre ${user.username}`) + .setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}${url}`) + .setStyle(ButtonStyle.Link), + ); + const msg = await generalChannel.send({ + embeds: [embed], + components: [row], + }); + queueMessagesEndpoints[playerId] = msg.id; + } catch (e) { + console.error(`Failed to post queue message for ${title}:`, e); + } } -async function updateDiscordMessage(client, game, title, resultText = '') { - const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null); - if (!channel) return null; +async function updateDiscordMessage(client, game, title, resultText = "") { + const channel = await client.channels.fetch(process.env.GENERAL_CHANNEL_ID).catch(() => null); + if (!channel) return null; - let description; - if (title === 'Tic Tac Toe') { - let gridText = ''; - for (let i = 1; i <= 9; i++) { gridText += game.xs.includes(i) ? '❌' : game.os.includes(i) ? '⭕' : '🟦'; if (i % 3 === 0) gridText += '\n'; } - description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`; - } else { - description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`; - } - if (resultText) description += `\n### ${resultText}`; + let description; + if (title === "Tic Tac Toe") { + let gridText = ""; + for (let i = 1; i <= 9; i++) { + gridText += game.xs.includes(i) ? "❌" : game.os.includes(i) ? "⭕" : "🟦"; + if (i % 3 === 0) gridText += "\n"; + } + description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`; + } else { + description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`; + } + if (resultText) description += `\n### ${resultText}`; - const embed = new EmbedBuilder().setTitle(title).setDescription(description).setColor(game.gameOver ? '#2ade2a' : '#5865f2'); + const embed = new EmbedBuilder() + .setTitle(title) + .setDescription(description) + .setColor(game.gameOver ? "#2ade2a" : "#5865f2"); - try { - if (game.msgId) { - const message = await channel.messages.fetch(game.msgId); - await message.edit({ embeds: [embed] }); - return game.msgId; - } else { - const message = await channel.send({ embeds: [embed] }); - return message.id; - } - } catch (e) { return null; } + try { + if (game.msgId) { + const message = await channel.messages.fetch(game.msgId); + await message.edit({ embeds: [embed] }); + return game.msgId; + } else { + const message = await channel.send({ embeds: [embed] }); + return message.id; + } + } catch (e) { + return null; + } } function cleanupStaleGames() { - const now = Date.now(); - const STALE_TIMEOUT = 30 * 60 * 1000; - const cleanup = (games, name) => { - Object.keys(games).forEach(key => { - if (now - games[key].lastmove > STALE_TIMEOUT) { - console.log(`[Cleanup] Removing stale ${name} game: ${key}`); - delete games[key]; - } - }); - }; - cleanup(activeTicTacToeGames, 'TicTacToe'); - cleanup(activeConnect4Games, 'Connect4'); + const now = Date.now(); + const STALE_TIMEOUT = 30 * 60 * 1000; + const cleanup = (games, name) => { + Object.keys(games).forEach((key) => { + if (now - games[key].lastmove > STALE_TIMEOUT) { + console.log(`[Cleanup] Removing stale ${name} game: ${key}`); + delete games[key]; + } + }); + }; + cleanup(activeTicTacToeGames, "TicTacToe"); + cleanup(activeConnect4Games, "Connect4"); } /* EMITS */ export async function socketEmit(event, data) { - io.emit(event, data); + io.emit(event, data); } export async function emitDataUpdated(data) { - io.emit('data-updated', data); + io.emit("data-updated", data); } export async function emitPokerUpdate(data) { - io.emit('poker-update', data); + io.emit("poker-update", data); } export async function emitPokerToast(data) { - io.emit('poker-toast', data); + io.emit("poker-toast", data); } export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room }); -export const emitToast = (payload) => io.emit("blackjack:toast", payload); +export const emitToast = (payload) => io.emit("blackjack:toast", payload); -export const emitSolitaireUpdate = (userId, moves) => io.emit('solitaire:update', {userId, moves}); \ No newline at end of file +export const emitSolitaireUpdate = (userId, moves) => io.emit("solitaire:update", { userId, moves }); diff --git a/src/utils/ai.js b/src/utils/ai.js index dd4ad99..401438a 100644 --- a/src/utils/ai.js +++ b/src/utils/ai.js @@ -1,27 +1,26 @@ -import 'dotenv/config'; +import "dotenv/config"; import OpenAI from "openai"; -import {GoogleGenAI} from "@google/genai"; -import {Mistral} from '@mistralai/mistralai'; +import { GoogleGenAI } from "@google/genai"; +import { Mistral } from "@mistralai/mistralai"; // --- AI Client Initialization --- // Initialize clients for each AI service. This is done once when the module is loaded. let openai; if (process.env.OPENAI_API_KEY) { - openai = new OpenAI(); + openai = new OpenAI(); } let gemini; if (process.env.GEMINI_KEY) { - gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY}) + gemini = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY }); } let mistral; if (process.env.MISTRAL_KEY) { - mistral = new Mistral({apiKey: process.env.MISTRAL_KEY}); + mistral = new Mistral({ apiKey: process.env.MISTRAL_KEY }); } - /** * Gets a response from the configured AI model. * It dynamically chooses the provider based on the MODEL environment variable. @@ -29,175 +28,180 @@ if (process.env.MISTRAL_KEY) { * @returns {Promise} The content of the AI's response message. */ export async function gork(messageHistory) { - const modelProvider = process.env.MODEL; + const modelProvider = process.env.MODEL; - console.log(`[AI] Requesting completion from ${modelProvider}...`); + console.log(`[AI] Requesting completion from ${modelProvider}...`); - try { - // --- OpenAI Provider --- - if (modelProvider === 'OpenAI' && openai) { - const completion = await openai.chat.completions.create({ - model: "gpt-5", // Using a modern, cost-effective model - reasoning_effort: "low", - messages: messageHistory, - }); - return completion.choices[0].message.content; - } + try { + // --- OpenAI Provider --- + if (modelProvider === "OpenAI" && openai) { + const completion = await openai.chat.completions.create({ + model: "gpt-5", // Using a modern, cost-effective model + reasoning_effort: "low", + messages: messageHistory, + }); + return completion.choices[0].message.content; + } - // --- Google Gemini Provider --- - else if (modelProvider === 'Gemini' && gemini) { - // Gemini requires a slightly different history format. - const contents = messageHistory.map(msg => ({ - role: msg.role === 'assistant' ? 'model' : msg.role, // Gemini uses 'model' for assistant role - parts: [{ text: msg.content }], - })); + // --- Google Gemini Provider --- + else if (modelProvider === "Gemini" && gemini) { + // Gemini requires a slightly different history format. + const contents = messageHistory.map((msg) => ({ + role: msg.role === "assistant" ? "model" : msg.role, // Gemini uses 'model' for assistant role + parts: [{ text: msg.content }], + })); - // The last message should not be from the model - if (contents[contents.length - 1].role === 'model') { - contents.pop(); - } + // The last message should not be from the model + if (contents[contents.length - 1].role === "model") { + contents.pop(); + } - const result = await gemini.generateContent({ contents }); - const response = await result.response; - return response.text(); - } + const result = await gemini.generateContent({ contents }); + const response = await result.response; + return response.text(); + } - // --- Mistral Provider --- - else if (modelProvider === 'Mistral' && mistral) { - const chatResponse = await mistral.chat({ - model: 'mistral-large-latest', - messages: messageHistory, - }); - return chatResponse.choices[0].message.content; - } + // --- Mistral Provider --- + else if (modelProvider === "Mistral" && mistral) { + const chatResponse = await mistral.chat({ + model: "mistral-large-latest", + messages: messageHistory, + }); + return chatResponse.choices[0].message.content; + } - // --- Fallback Case --- - else { - console.warn(`[AI] No valid AI provider configured or API key missing for MODEL=${modelProvider}.`); - return "Le service d'IA n'est pas correctement configuré. Veuillez contacter l'administrateur."; - } - } catch(error) { - console.error(`[AI] Error with ${modelProvider} API:`, error); - return "Oups, une erreur est survenue en contactant le service d'IA."; - } + // --- Fallback Case --- + else { + console.warn(`[AI] No valid AI provider configured or API key missing for MODEL=${modelProvider}.`); + return "Le service d'IA n'est pas correctement configuré. Veuillez contacter l'administrateur."; + } + } catch (error) { + console.error(`[AI] Error with ${modelProvider} API:`, error); + return "Oups, une erreur est survenue en contactant le service d'IA."; + } } -export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || '100', 10); -export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || '3', 10); -export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || 'true') === 'true'; +export const CONTEXT_LIMIT = parseInt(process.env.AI_CONTEXT_MESSAGES || "100", 10); +export const MAX_ATTS_PER_MESSAGE = parseInt(process.env.AI_MAX_ATTS_PER_MSG || "3", 10); +export const INCLUDE_ATTACHMENT_URLS = (process.env.AI_INCLUDE_ATTACHMENT_URLS || "true") === "true"; -export const stripMentionsOfBot = (text, botId) => - text.replace(new RegExp(`<@!?${botId}>`, 'g'), '').trim(); +export const stripMentionsOfBot = (text, botId) => text.replace(new RegExp(`<@!?${botId}>`, "g"), "").trim(); export const sanitize = (s) => - (s || '') - .replace(/\s+/g, ' ') - .replace(/```/g, 'ʼʼʼ') // éviter de casser des fences éventuels - .trim(); + (s || "") + .replace(/\s+/g, " ") + .replace(/```/g, "ʼʼʼ") // éviter de casser des fences éventuels + .trim(); export const shortTs = (d) => new Date(d).toISOString(); // compact et triable export function buildParticipantsMap(messages) { - const map = {}; - for (const m of messages) { - const id = m.author.id; - if (!map[id]) { - map[id] = { - id, - username: m.author.username, - globalName: m.author.globalName || null, - isBot: !!m.author.bot, - }; - } - } - return map; + const map = {}; + for (const m of messages) { + const id = m.author.id; + if (!map[id]) { + map[id] = { + id, + username: m.author.username, + globalName: m.author.globalName || null, + isBot: !!m.author.bot, + }; + } + } + return map; } export function buildTranscript(messages, botId) { - // Oldest -> newest, JSONL compact, une ligne par message pertinent - const lines = []; - for (const m of messages) { - const content = sanitize(m.content); - const atts = Array.from(m.attachments?.values?.() || []); - if (!content && atts.length === 0) continue; + // Oldest -> newest, JSONL compact, une ligne par message pertinent + const lines = []; + for (const m of messages) { + const content = sanitize(m.content); + const atts = Array.from(m.attachments?.values?.() || []); + if (!content && atts.length === 0) continue; - const attMeta = atts.length - ? atts.slice(0, MAX_ATTS_PER_MESSAGE).map(a => ({ - id: a.id, - name: a.name, - type: a.contentType || 'application/octet-stream', - size: a.size, - isImage: !!(a.contentType && a.contentType.startsWith('image/')), - width: a.width || undefined, - height: a.height || undefined, - spoiler: typeof a.spoiler === 'boolean' ? a.spoiler : false, - url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut - })) - : undefined; + const attMeta = atts.length + ? atts.slice(0, MAX_ATTS_PER_MESSAGE).map((a) => ({ + id: a.id, + name: a.name, + type: a.contentType || "application/octet-stream", + size: a.size, + isImage: !!(a.contentType && a.contentType.startsWith("image/")), + width: a.width || undefined, + height: a.height || undefined, + spoiler: typeof a.spoiler === "boolean" ? a.spoiler : false, + url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined, // désactive par défaut + })) + : undefined; - const line = { - t: shortTs(m.createdTimestamp || Date.now()), - id: m.author.id, - nick: m.member?.nickname || m.author.globalName || m.author.username, - isBot: !!m.author.bot, - mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ''), - replyTo: m.reference?.messageId || null, - content, - attachments: attMeta, - }; - lines.push(line); - } - return lines.map(l => JSON.stringify(l)).join('\n'); + const line = { + t: shortTs(m.createdTimestamp || Date.now()), + id: m.author.id, + nick: m.member?.nickname || m.author.globalName || m.author.username, + isBot: !!m.author.bot, + mentionsBot: new RegExp(`<@!?${botId}>`).test(m.content || ""), + replyTo: m.reference?.messageId || null, + content, + attachments: attMeta, + }; + lines.push(line); + } + return lines.map((l) => JSON.stringify(l)).join("\n"); } export function buildAiMessages({ - botId, - botName = 'FlopoBot', - invokerId, - invokerName, - requestText, - transcript, - participants, - repliedUserId, - invokerAttachments = [], + botId, + botName = "FlopoBot", + invokerId, + invokerName, + requestText, + transcript, + participants, + repliedUserId, + invokerAttachments = [], }) { - const system = { - role: 'system', - content: - `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote. + const system = { + role: "system", + content: `Tu es ${botName} (ID: ${botId}) sur un serveur Discord. Style: bref, naturel, détendu, comme un pote. Règles de sortie: - Réponds en français, en 1–3 phrases. - Réponds PRINCIPALEMENT au message de <@${invokerId}>. Le transcript est un contexte facultatif. - Pas de "Untel a dit…", pas de longs préambules. - Utilise <@ID> pour mentionner quelqu'un. - Tu ne peux PAS ouvrir les liens; si des pièces jointes existent, tu peux simplement les mentionner (ex: "ta photo", "le PDF").`, - }; + }; - const attLines = invokerAttachments.length - ? invokerAttachments.map(a => `- ${a.name} (${a.type || 'type inconnu'}, ${a.size ?? '?'} o${a.isImage ? ', image' : ''})`).join('\n') - : ''; + const attLines = invokerAttachments.length + ? invokerAttachments + .map((a) => `- ${a.name} (${a.type || "type inconnu"}, ${a.size ?? "?"} o${a.isImage ? ", image" : ""})`) + .join("\n") + : ""; - const user = { - role: 'user', - content: - `Tâche: répondre brièvement à <@${invokerId}>. + const user = { + role: "user", + content: `Tâche: répondre brièvement à <@${invokerId}>. - Message de <@${invokerId}> (${invokerName || 'inconnu'}): + Message de <@${invokerId}> (${invokerName || "inconnu"}): """ ${requestText} """ - ${invokerAttachments.length ? `Pièces jointes du message: + ${ + invokerAttachments.length + ? `Pièces jointes du message: ${attLines} - ` : ''}${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ''} + ` + : "" + }${repliedUserId ? `Ce message répond à <@${repliedUserId}>.` : ""} Participants (id -> nom): - ${Object.values(participants).map(p => `- ${p.id} -> ${p.globalName || p.username}`).join('\n')} + ${Object.values(participants) + .map((p) => `- ${p.id} -> ${p.globalName || p.username}`) + .join("\n")} Contexte (transcript JSONL; à utiliser seulement si utile): \`\`\`jsonl ${transcript} \`\`\``, - }; + }; - return [system, user]; -} \ No newline at end of file + return [system, user]; +} diff --git a/src/utils/erinyes.js b/src/utils/erinyes.js index efbe475..178d283 100644 --- a/src/utils/erinyes.js +++ b/src/utils/erinyes.js @@ -1,75 +1,75 @@ export const roles = { - erynie_1: { - name: 'Erinye', - subtitle: 'Mégère, la haine', - descr: '', - powers: { - double_vote: { - descr: 'Les Erinyes peuvent tuer une deuxième personne (1 seule fois).', - charges: 1, - disabled: false, - }, - }, - passive: {}, - team: 'Erinyes', - }, - erynie_2: { - name: 'Erinye', - subtitle: 'Tisiphone, la vengeance', - descr: '', - powers: { - one_shot: { - descr: 'Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).', - charges: 1, - disabled: false, - }, - }, - passive: {}, - team: 'Erinyes', - }, - erynie_3: { - name: 'Erinye', - subtitle: 'Alecto, l\'implacable', - descr: '', - powers: { - silence: { - descr: 'Empêche l\'utilisation du pouvoir de quelqu\'un pour le prochain tour.', - charges: 999, - disabled: false, - } - }, - passive: { - descr: 'Voit quels pouvoirs ont été utilisés.', - disabled: false, - }, - team: 'Erinyes', - }, - narcisse: { - name: 'Narcisse', - subtitle: '', - descr: '', - powers: {}, - passive: { - descr: 'S\'il devient maire ...', - disabled: false, - }, - }, - charon: { - name: 'Charon', - subtitle: 'Sorcier', - descr: 'C\'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.', - powers: { - revive: { - descr: 'Refuser de faire traverser le Styx (sauver quelqu\'un)', - charges: 1, - disabled: false, - }, - kill: { - descr: 'Traverser le Styx (tuer quelqu\'un)', - charges: 1, - disabled: false, - } - }, - }, - //... -} \ No newline at end of file + erynie_1: { + name: "Erinye", + subtitle: "Mégère, la haine", + descr: "", + powers: { + double_vote: { + descr: "Les Erinyes peuvent tuer une deuxième personne (1 seule fois).", + charges: 1, + disabled: false, + }, + }, + passive: {}, + team: "Erinyes", + }, + erynie_2: { + name: "Erinye", + subtitle: "Tisiphone, la vengeance", + descr: "", + powers: { + one_shot: { + descr: "Tuer une personne de son choix en plus du vote des Erinyes (1 seule fois).", + charges: 1, + disabled: false, + }, + }, + passive: {}, + team: "Erinyes", + }, + erynie_3: { + name: "Erinye", + subtitle: "Alecto, l'implacable", + descr: "", + powers: { + silence: { + descr: "Empêche l'utilisation du pouvoir de quelqu'un pour le prochain tour.", + charges: 999, + disabled: false, + }, + }, + passive: { + descr: "Voit quels pouvoirs ont été utilisés.", + disabled: false, + }, + team: "Erinyes", + }, + narcisse: { + name: "Narcisse", + subtitle: "", + descr: "", + powers: {}, + passive: { + descr: "S'il devient maire ...", + disabled: false, + }, + }, + charon: { + name: "Charon", + subtitle: "Sorcier", + descr: "C'est le passeur, il est appelé chaque nuit après les Erinyes pour décider du sort des mortels.", + powers: { + revive: { + descr: "Refuser de faire traverser le Styx (sauver quelqu'un)", + charges: 1, + disabled: false, + }, + kill: { + descr: "Traverser le Styx (tuer quelqu'un)", + charges: 1, + disabled: false, + }, + }, + }, + //... +}; diff --git a/src/utils/index.js b/src/utils/index.js index 20890a1..f8f246e 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,27 +1,30 @@ -import 'dotenv/config'; -import cron from 'node-cron'; -import { adjectives, animals, uniqueNamesGenerator } from 'unique-names-generator'; +import "dotenv/config"; +import cron from "node-cron"; // --- Local Imports --- -import { getValorantSkins, getSkinTiers } from '../api/valorant.js'; -import { DiscordRequest } from '../api/discord.js'; -import { initTodaysSOTD } from '../game/points.js'; +import { getSkinTiers, getValorantSkins } from "../api/valorant.js"; +import { DiscordRequest } from "../api/discord.js"; +import { initTodaysSOTD } from "../game/points.js"; import { - insertManyUsers, insertManySkins, resetDailyReward, - pruneOldLogs, getAllUsers as dbGetAllUsers, getSOTD, getUser, getAllUsers, insertUser, stmtUsers, -} from '../database/index.js'; -import { activeInventories, activeSearchs, activePredis, pokerRooms, skins } from '../game/state.js'; + getAllAkhys, + getAllUsers, + insertManySkins, + insertUser, + resetDailyReward, + updateUserAvatar, +} from "../database/index.js"; +import { activeInventories, activeSearchs, skins } from "../game/state.js"; export async function InstallGlobalCommands(appId, commands) { - // API endpoint to overwrite global commands - const endpoint = `applications/${appId}/commands`; + // API endpoint to overwrite global commands + const endpoint = `applications/${appId}/commands`; - try { - // This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands - await DiscordRequest(endpoint, { method: 'PUT', body: commands }); - } catch (err) { - console.error(err); - } + try { + // This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands + await DiscordRequest(endpoint, { method: "PUT", body: commands }); + } catch (err) { + console.error(err); + } } // --- Data Fetching & Initialization --- @@ -32,74 +35,74 @@ export async function InstallGlobalCommands(appId, commands) { * @param {object} client - The Discord.js client instance. */ export async function getAkhys(client) { - try { - // 1. Fetch Discord Members - const initial_akhys = getAllUsers.all().length; - const guild = await client.guilds.fetch(process.env.GUILD_ID); - const members = await guild.members.fetch(); - const akhys = members.filter(m => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID)); + try { + // 1. Fetch Discord Members + const initial_akhys = getAllUsers.all().length; + const guild = await client.guilds.fetch(process.env.GUILD_ID); + const members = await guild.members.fetch(); + const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID)); + const usersToInsert = akhys.map((akhy) => ({ + id: akhy.user.id, + username: akhy.user.username, + globalName: akhy.user.globalName, + warned: 0, + warns: 0, + allTimeWarns: 0, + totalRequests: 0, + avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }), + isAkhy: 1, + })); - const usersToInsert = akhys.map(akhy => ({ - id: akhy.user.id, - username: akhy.user.username, - globalName: akhy.user.globalName, - warned: 0, - warns: 0, - allTimeWarns: 0, - totalRequests: 0, - avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }), - isAkhy: 1 - })); + if (usersToInsert.length > 0) { + usersToInsert.forEach((user) => { + try { + insertUser.run(user); + } catch (err) {} + }); + } - if (usersToInsert.length > 0) { - usersToInsert.forEach(user => { - try { insertUser.run(user) } catch (err) {} - }) - } + const new_akhys = getAllUsers.all().length; + const diff = new_akhys - initial_akhys; + console.log( + `[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? "(" + (diff > 0 ? "+" + diff : diff) + ") " : ""}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`, + ); - const new_akhys = getAllUsers.all().length; - const diff = new_akhys - initial_akhys - - console.log(`[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? '(' + (diff > 0 ? '+' + diff : diff) + ') ' : ''}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`); + // 2. Fetch Valorant Skins + const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]); - // 2. Fetch Valorant Skins - const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]); + // Clear and rebuild the in-memory skin cache + skins.length = 0; + fetchedSkins.forEach((skin) => skins.push(skin)); - // Clear and rebuild the in-memory skin cache - skins.length = 0; - fetchedSkins.forEach(skin => skins.push(skin)); + const skinsToInsert = fetchedSkins + .filter((skin) => skin.contentTierUuid) + .map((skin) => { + const tier = fetchedTiers.find((t) => t.uuid === skin.contentTierUuid) || {}; + const basePrice = calculateBasePrice(skin, tier.rank); + return { + uuid: skin.uuid, + displayName: skin.displayName, + contentTierUuid: skin.contentTierUuid, + displayIcon: skin.displayIcon, + user_id: null, + tierRank: tier.rank, + tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3", + tierText: formatTierText(tier.rank, skin.displayName), + basePrice: basePrice.toFixed(0), + maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0), + }; + }); - const skinsToInsert = fetchedSkins - .filter(skin => skin.contentTierUuid) - .map(skin => { - const tier = fetchedTiers.find(t => t.uuid === skin.contentTierUuid) || {}; - const basePrice = calculateBasePrice(skin, tier.rank); - return { - uuid: skin.uuid, - displayName: skin.displayName, - contentTierUuid: skin.contentTierUuid, - displayIcon: skin.displayIcon, - user_id: null, - tierRank: tier.rank, - tierColor: tier.highlightColor?.slice(0, 6) || 'F2F3F3', - tierText: formatTierText(tier.rank, skin.displayName), - basePrice: basePrice.toFixed(0), - maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0), - }; - }); - - if (skinsToInsert.length > 0) { - insertManySkins(skinsToInsert); - } - console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`); - - } catch (err) { - console.error('Error during initial data sync (getAkhys):', err); - } + if (skinsToInsert.length > 0) { + insertManySkins(skinsToInsert); + } + console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`); + } catch (err) { + console.error("Error during initial data sync (getAkhys):", err); + } } - // --- Cron Jobs / Scheduled Tasks --- /** @@ -108,72 +111,87 @@ export async function getAkhys(client) { * @param {object} io - The Socket.IO server instance. */ export function setupCronJobs(client, io) { - // Every 10 minutes: Clean up expired interactive sessions - cron.schedule('*/10 * * * *', () => { - const now = Date.now(); - const FIVE_MINUTES = 5 * 60 * 1000; - const ONE_DAY = 24 * 60 * 60 * 1000; + // Every 10 minutes: Clean up expired interactive sessions + cron.schedule("*/10 * * * *", () => { + const now = Date.now(); + const FIVE_MINUTES = 5 * 60 * 1000; + const ONE_DAY = 24 * 60 * 60 * 1000; - const cleanup = (sessions, name) => { - let cleanedCount = 0; - for (const id in sessions) { - if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) { - delete sessions[id]; - cleanedCount++; - } - } - if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`); - }; + const cleanup = (sessions, name) => { + let cleanedCount = 0; + for (const id in sessions) { + if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) { + delete sessions[id]; + cleanedCount++; + } + } + if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`); + }; - cleanup(activeInventories, 'inventory'); - cleanup(activeSearchs, 'search'); + cleanup(activeInventories, "inventory"); + cleanup(activeSearchs, "search"); - // Cleanup for predis and poker rooms... - // ... - }); + // TODO: Cleanup for predis and poker rooms... + // ... + }); - // Daily at midnight: Reset daily rewards and init SOTD - cron.schedule('0 0 * * *', async () => { - console.log('[Cron] Running daily midnight tasks...'); - try { - resetDailyReward.run(); - console.log('[Cron] Daily rewards have been reset for all users.'); - //if (!getSOTD.get()) { - initTodaysSOTD(); - //} - } catch (e) { - console.error('[Cron] Error during daily reset:', e); - } - }); + // Daily at midnight: Reset daily rewards and init SOTD + cron.schedule("0 0 * * *", async () => { + console.log("[Cron] Running daily midnight tasks..."); + try { + resetDailyReward.run(); + console.log("[Cron] Daily rewards have been reset for all users."); + //if (!getSOTD.get()) { + initTodaysSOTD(); + //} + } catch (e) { + console.error("[Cron] Error during daily reset:", e); + } + }); - // Daily at 7 AM: Re-sync users and skins - cron.schedule('0 7 * * *', async () => { - console.log('[Cron] Running daily 7 AM data sync...'); - await getAkhys(client); - }); + // Daily at 7 AM: Re-sync users and skins + cron.schedule("0 7 * * *", async () => { + console.log("[Cron] Running daily 7 AM data sync..."); + await getAkhys(client); + try { + const akhys = getAllAkhys.all(); + for (const akhy of akhys) { + const user = await client.users.cache.get(akhy.id); + try { + updateUserAvatar.run({ + id: akhy.id, + avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }), + }); + } catch (err) { + console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err); + } + } + } catch (e) { + console.error("[Cron] Error during daily avatar update:", e); + } + }); } - // --- Formatting Helpers --- export function capitalize(str) { - if (typeof str !== 'string' || str.length === 0) return ''; - return str.charAt(0).toUpperCase() + str.slice(1); + if (typeof str !== "string" || str.length === 0) return ""; + return str.charAt(0).toUpperCase() + str.slice(1); } export function formatTime(seconds) { - const d = Math.floor(seconds / (3600*24)); - const h = Math.floor(seconds % (3600*24) / 3600); - const m = Math.floor(seconds % 3600 / 60); - const s = Math.floor(seconds % 60); + const d = Math.floor(seconds / (3600 * 24)); + const h = Math.floor((seconds % (3600 * 24)) / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = Math.floor(seconds % 60); - const parts = []; - if (d > 0) parts.push(`**${d}** jour${d > 1 ? 's' : ''}`); - if (h > 0) parts.push(`**${h}** heure${h > 1 ? 's' : ''}`); - if (m > 0) parts.push(`**${m}** minute${m > 1 ? 's' : ''}`); - if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? 's' : ''}`); + const parts = []; + if (d > 0) parts.push(`**${d}** jour${d > 1 ? "s" : ""}`); + if (h > 0) parts.push(`**${h}** heure${h > 1 ? "s" : ""}`); + if (m > 0) parts.push(`**${m}** minute${m > 1 ? "s" : ""}`); + if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? "s" : ""}`); - return parts.join(', ').replace(/,([^,]*)$/, ' et$1'); + return parts.join(", ").replace(/,([^,]*)$/, " et$1"); } // --- External API Helpers --- @@ -182,15 +200,15 @@ export function formatTime(seconds) { * Fetches user data from the "APO" service. */ export async function getAPOUsers() { - const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`; - try { - const response = await fetch(fetchUrl); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - return await response.json(); - } catch (error) { - console.error('Error fetching APO users:', error); - return null; - } + const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`; + try { + const response = await fetch(fetchUrl); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return await response.json(); + } catch (error) { + console.error("Error fetching APO users:", error); + return null; + } } /** @@ -199,112 +217,124 @@ export async function getAPOUsers() { * @param {number} amount - The amount to "buy". */ export async function postAPOBuy(userId, amount) { - const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`; - return fetch(fetchUrl, { method: 'POST' }); + const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`; + return fetch(fetchUrl, { method: "POST" }); } - // --- Miscellaneous Helpers --- export async function getOnlineUsersWithRole(guild, roleId) { - if (!guild || !roleId) return new Map(); - try { - const members = await guild.members.fetch(); - return members.filter(m => !m.user.bot && m.presence?.status !== 'offline' && m.presence?.status !== undefined && m.roles.cache.has(roleId)); - } catch (err) { - console.error('Error fetching online members with role:', err); - return new Map(); - } + if (!guild || !roleId) return new Map(); + try { + const members = await guild.members.fetch(); + return members.filter( + (m) => + !m.user.bot && + m.presence?.status !== "offline" && + m.presence?.status !== undefined && + m.roles.cache.has(roleId), + ); + } catch (err) { + console.error("Error fetching online members with role:", err); + return new Map(); + } } export function getRandomEmoji(list = 0) { - const emojiLists = [ - ['😭','😄','😌','🤓','😎','😤','🤖','😶‍🌫️','🌏','📸','💿','👋','🌊','✨'], - ['<:CAUGHT:1323810730155446322>', '<:hinhinhin:1072510144933531758>', '<:o7:1290773422451986533>', '<:zhok:1115221772623683686>', '<:nice:1154049521110765759>', '<:nerd:1087658195603951666>', '<:peepSelfie:1072508131839594597>'], - ]; - const selectedList = emojiLists[list] || ['']; - return selectedList[Math.floor(Math.random() * selectedList.length)]; + const emojiLists = [ + ["😭", "😄", "😌", "🤓", "😎", "😤", "🤖", "😶‍🌫️", "🌏", "📸", "💿", "👋", "🌊", "✨"], + [ + "<:CAUGHT:1323810730155446322>", + "<:hinhinhin:1072510144933531758>", + "<:o7:1290773422451986533>", + "<:zhok:1115221772623683686>", + "<:nice:1154049521110765759>", + "<:nerd:1087658195603951666>", + "<:peepSelfie:1072508131839594597>", + ], + ]; + const selectedList = emojiLists[list] || [""]; + return selectedList[Math.floor(Math.random() * selectedList.length)]; } export function formatAmount(amount) { - if (amount >= 1000000000) { - amount /= 1000000000 - return ( - amount - .toFixed(2) - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'Md' - ) - } - if (amount >= 1000000) { - amount /= 1000000 - return ( - amount - .toFixed(2) - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'M' - ) - } - if (amount >= 10000) { - amount /= 1000 - return ( - amount - .toFixed(2) - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + 'K' - ) - } - return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + if (amount >= 1000000000) { + amount /= 1000000000; + return ( + amount + .toFixed(2) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "Md" + ); + } + if (amount >= 1000000) { + amount /= 1000000; + return ( + amount + .toFixed(2) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "M" + ); + } + if (amount >= 10000) { + amount /= 1000; + return ( + amount + .toFixed(2) + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "K" + ); + } + return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " "); } - // --- Private Helpers --- export function calculateBasePrice(skin, tierRank) { - const name = skin.displayName.toLowerCase(); - let price = 6000; // Default for melee - if (name.includes('classic')) price = 150; - else if (name.includes('shorty')) price = 300; - else if (name.includes('frenzy')) price = 450; - else if (name.includes('ghost')) price = 500; - else if (name.includes('sheriff')) price = 800; - else if (name.includes('stinger')) price = 1000; - else if (name.includes('spectre')) price = 1600; - else if (name.includes('bucky')) price = 900; - else if (name.includes('judge')) price = 1500; - else if (name.includes('bulldog')) price = 2100; - else if (name.includes('guardian')) price = 2700 - else if (name.includes('vandal') || name.includes('phantom')) price = 2900; - else if (name.includes('marshal')) price = 950; - else if (name.includes('outlaw')) price = 2400; - else if (name.includes('operator')) price = 4500; - else if (name.includes('ares')) price = 1700; - else if (name.includes('odin')) price = 3200; + const name = skin.displayName.toLowerCase(); + let price = 6000; // Default for melee + if (name.includes("classic")) price = 150; + else if (name.includes("shorty")) price = 300; + else if (name.includes("frenzy")) price = 450; + else if (name.includes("ghost")) price = 500; + else if (name.includes("sheriff")) price = 800; + else if (name.includes("stinger")) price = 1000; + else if (name.includes("spectre")) price = 1600; + else if (name.includes("bucky")) price = 900; + else if (name.includes("judge")) price = 1500; + else if (name.includes("bulldog")) price = 2100; + else if (name.includes("guardian")) price = 2700; + else if (name.includes("vandal") || name.includes("phantom")) price = 2900; + else if (name.includes("marshal")) price = 950; + else if (name.includes("outlaw")) price = 2400; + else if (name.includes("operator")) price = 4500; + else if (name.includes("ares")) price = 1700; + else if (name.includes("odin")) price = 3200; - price *= (1 + (tierRank || 0)); - if (name.includes('vct')) price *= 1.25; - if (name.includes('champions')) price *= 2; + price *= 1 + (tierRank || 0); + if (name.includes("vct")) price *= 1.25; + if (name.includes("champions")) price *= 2; - return price / 124; + return price / 124; } export function calculateMaxPrice(basePrice, skin) { - let res = basePrice; - res *= (1 + (skin.levels.length / Math.max(skin.levels.length, 2))); - res *= (1 + (skin.chromas.length / 4)); - return res; + let res = basePrice; + res *= 1 + skin.levels.length / Math.max(skin.levels.length, 2); + res *= 1 + skin.chromas.length / 4; + return res; } function formatTierText(rank, displayName) { - const tiers = { - 0: '**<:select:1362964319498670222> Select**', - 1: '**<:deluxe:1362964308094488797> Deluxe**', - 2: '**<:premium:1362964330349330703> Premium**', - 3: '**<:exclusive:1362964427556651098> Exclusive**', - 4: '**<:ultra:1362964339685986314> Ultra**', - }; - let res = tiers[rank] || 'Pas de tier'; - if (displayName.includes('VCT')) res += ' | Esports'; - if (displayName.toLowerCase().includes('champions')) res += ' | Champions'; - return res; -} \ No newline at end of file + const tiers = { + 0: "**<:select:1362964319498670222> Select**", + 1: "**<:deluxe:1362964308094488797> Deluxe**", + 2: "**<:premium:1362964330349330703> Premium**", + 3: "**<:exclusive:1362964427556651098> Exclusive**", + 4: "**<:ultra:1362964339685986314> Ultra**", + }; + let res = tiers[rank] || "Pas de tier"; + if (displayName.includes("VCT")) res += " | Esports"; + if (displayName.toLowerCase().includes("champions")) res += " | Champions"; + return res; +}