diff --git a/.gitignore b/.gitignore index 3db8cf7..33f481f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ flopobot.db flopobot.db-shm flopobot.db-wal .idea -*.db \ No newline at end of file +*.db +.claude diff --git a/package-lock.json b/package-lock.json index 8d48a81..907a8f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@google/genai": "^1.30.0", "@mistralai/mistralai": "^1.6.0", + "@prisma/client": "^6.19.2", "axios": "^1.9.0", - "better-sqlite3": "^11.9.1", "discord-interactions": "^4.0.0", "discord.js": "^14.18.0", "dotenv": "^16.0.3", @@ -20,6 +20,7 @@ "node-cron": "^3.0.3", "openai": "^4.104.0", "pokersolver": "^2.1.4", + "prisma": "^6.19.2", "socket.io": "^4.8.1", "stripe": "^20.3.0", "unique-names-generator": "^4.7.1", @@ -523,6 +524,85 @@ "node": ">=14" } }, + "node_modules/@prisma/client": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.18.4", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.2" + } + }, "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -562,6 +642,12 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", @@ -815,17 +901,6 @@ "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.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", @@ -848,26 +923,6 @@ "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.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -945,30 +1000,6 @@ "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", @@ -984,6 +1015,62 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/c12/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "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", @@ -1088,11 +1175,14 @@ "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/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } }, "node_modules/color-convert": { "version": "2.0.1", @@ -1131,6 +1221,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -1212,30 +1317,6 @@ "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", @@ -1243,6 +1324,21 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "license": "MIT" + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1261,6 +1357,12 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -1271,15 +1373,6 @@ "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.38", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.38.tgz", @@ -1326,9 +1419,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -1372,12 +1465,31 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", + "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1387,15 +1499,6 @@ "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", @@ -1752,15 +1855,6 @@ "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.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -1807,12 +1901,40 @@ "url": "https://opencollective.com/express" } }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, "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-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1878,12 +2000,6 @@ "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", @@ -2054,12 +2170,6 @@ "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", @@ -2168,11 +2278,22 @@ "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/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } }, "node_modules/glob": { "version": "10.5.0", @@ -2418,26 +2539,6 @@ "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", @@ -2488,12 +2589,6 @@ "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", @@ -2579,6 +2674,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -2783,18 +2887,6 @@ "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", @@ -2808,15 +2900,6 @@ "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/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -2826,24 +2909,12 @@ "node": ">=16 || 14 >=14.17" } }, - "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", @@ -2860,18 +2931,6 @@ "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", @@ -2932,6 +2991,12 @@ } } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/nodemon": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", @@ -2996,6 +3061,29 @@ "node": ">=0.10.0" } }, + "node_modules/nypm": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.0", + "pathe": "^2.0.3", + "tinyexec": "^1.0.2" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", + "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3017,6 +3105,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -3029,15 +3123,6 @@ "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", @@ -3202,6 +3287,18 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3215,6 +3312,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/pokersolver": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/pokersolver/-/pokersolver-2.1.4.tgz", @@ -3224,32 +3332,6 @@ ], "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", @@ -3276,6 +3358,31 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prisma": { + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3302,16 +3409,6 @@ "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", @@ -3322,6 +3419,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -3390,33 +3503,14 @@ "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==", + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "defu": "^6.1.4", + "destr": "^2.0.3" } }, "node_modules/readdirp": { @@ -3487,6 +3581,7 @@ "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3660,51 +3755,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "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", @@ -3858,15 +3908,6 @@ "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/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -3963,15 +4004,6 @@ "node": ">=8" } }, - "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/stripe": { "version": "20.3.0", "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.0.tgz", @@ -4002,32 +4034,13 @@ "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==", + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "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": ">=18" } }, "node_modules/to-regex-range": { @@ -4080,18 +4093,6 @@ "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", @@ -4168,12 +4169,6 @@ "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", @@ -4343,12 +4338,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "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", diff --git a/package.json b/package.json index ec9da38..7149f5f 100644 --- a/package.json +++ b/package.json @@ -11,15 +11,17 @@ "scripts": { "start": "node index.js", "register": "node commands.js", - "dev": "nodemon index.js" + "dev": "nodemon index.js", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev" }, "author": "Milo Gourvest", "license": "MIT", "dependencies": { "@google/genai": "^1.30.0", "@mistralai/mistralai": "^1.6.0", + "@prisma/client": "^6.19.2", "axios": "^1.9.0", - "better-sqlite3": "^11.9.1", "discord-interactions": "^4.0.0", "discord.js": "^14.18.0", "dotenv": "^16.0.3", @@ -27,6 +29,7 @@ "node-cron": "^3.0.3", "openai": "^4.104.0", "pokersolver": "^2.1.4", + "prisma": "^6.19.2", "socket.io": "^4.8.1", "stripe": "^20.3.0", "unique-names-generator": "^4.7.1", diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000..3bf510d --- /dev/null +++ b/prisma/migrations/0_init/migration.sql @@ -0,0 +1,138 @@ +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL PRIMARY KEY, + "username" TEXT NOT NULL, + "globalName" TEXT, + "warned" INTEGER NOT NULL DEFAULT 0, + "warns" INTEGER NOT NULL DEFAULT 0, + "allTimeWarns" INTEGER NOT NULL DEFAULT 0, + "totalRequests" INTEGER NOT NULL DEFAULT 0, + "coins" INTEGER NOT NULL DEFAULT 0, + "dailyQueried" INTEGER NOT NULL DEFAULT 0, + "avatarUrl" TEXT, + "isAkhy" INTEGER NOT NULL DEFAULT 0 +); + +-- CreateTable +CREATE TABLE "skins" ( + "uuid" TEXT NOT NULL PRIMARY KEY, + "displayName" TEXT, + "contentTierUuid" TEXT, + "displayIcon" TEXT, + "user_id" TEXT, + "tierRank" TEXT, + "tierColor" TEXT, + "tierText" TEXT, + "basePrice" TEXT, + "currentLvl" INTEGER, + "currentChroma" INTEGER, + "currentPrice" INTEGER, + "maxPrice" INTEGER, + CONSTRAINT "skins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "market_offers" ( + "id" TEXT NOT NULL PRIMARY KEY, + "skin_uuid" TEXT NOT NULL, + "seller_id" TEXT NOT NULL, + "starting_price" INTEGER NOT NULL, + "buyout_price" INTEGER, + "final_price" INTEGER, + "status" TEXT NOT NULL, + "posted_at" TEXT DEFAULT '', + "opening_at" TEXT NOT NULL, + "closing_at" TEXT NOT NULL, + "buyer_id" TEXT, + CONSTRAINT "market_offers_skin_uuid_fkey" FOREIGN KEY ("skin_uuid") REFERENCES "skins" ("uuid") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "market_offers_seller_id_fkey" FOREIGN KEY ("seller_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "market_offers_buyer_id_fkey" FOREIGN KEY ("buyer_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "bids" ( + "id" TEXT NOT NULL PRIMARY KEY, + "bidder_id" TEXT NOT NULL, + "market_offer_id" TEXT NOT NULL, + "offer_amount" INTEGER NOT NULL, + "offered_at" TEXT DEFAULT '', + CONSTRAINT "bids_bidder_id_fkey" FOREIGN KEY ("bidder_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "bids_market_offer_id_fkey" FOREIGN KEY ("market_offer_id") REFERENCES "market_offers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "logs" ( + "id" TEXT NOT NULL PRIMARY KEY, + "user_id" TEXT NOT NULL, + "action" TEXT, + "target_user_id" TEXT, + "coins_amount" INTEGER, + "user_new_amount" INTEGER, + "created_at" TEXT DEFAULT '', + CONSTRAINT "logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "logs_target_user_id_fkey" FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "games" ( + "id" TEXT NOT NULL PRIMARY KEY, + "p1" TEXT NOT NULL, + "p2" TEXT, + "p1_score" INTEGER, + "p2_score" INTEGER, + "p1_elo" INTEGER, + "p2_elo" INTEGER, + "p1_new_elo" INTEGER, + "p2_new_elo" INTEGER, + "type" TEXT, + "timestamp" TEXT, + CONSTRAINT "games_p1_fkey" FOREIGN KEY ("p1") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "games_p2_fkey" FOREIGN KEY ("p2") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "elos" ( + "id" TEXT NOT NULL PRIMARY KEY, + "elo" INTEGER NOT NULL, + CONSTRAINT "elos_id_fkey" FOREIGN KEY ("id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "sotd" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "tableauPiles" TEXT, + "foundationPiles" TEXT, + "stockPile" TEXT, + "wastePile" TEXT, + "isDone" INTEGER NOT NULL DEFAULT 0, + "seed" TEXT +); + +-- CreateTable +CREATE TABLE "sotd_stats" ( + "id" TEXT NOT NULL PRIMARY KEY, + "user_id" TEXT NOT NULL, + "time" INTEGER, + "moves" INTEGER, + "score" INTEGER, + CONSTRAINT "sotd_stats_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "transactions" ( + "id" TEXT NOT NULL PRIMARY KEY, + "session_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "coins_amount" INTEGER NOT NULL, + "amount_cents" INTEGER NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'eur', + "customer_email" TEXT, + "customer_name" TEXT, + "payment_status" TEXT NOT NULL, + "created_at" TEXT DEFAULT '', + CONSTRAINT "transactions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "transactions_session_id_key" ON "transactions"("session_id"); + diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..85229eb --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,175 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id String @id + username String + globalName String? + warned Int @default(0) + warns Int @default(0) + allTimeWarns Int @default(0) + totalRequests Int @default(0) + coins Int @default(0) + dailyQueried Int @default(0) + avatarUrl String? + isAkhy Int @default(0) + + elo Elo? + skins Skin[] + sellerOffers MarketOffer[] @relation("Seller") + buyerOffers MarketOffer[] @relation("Buyer") + bids Bid[] + logs Log[] @relation("UserLogs") + targetLogs Log[] @relation("TargetUserLogs") + gamesAsP1 Game[] @relation("Player1") + gamesAsP2 Game[] @relation("Player2") + sotdStats SotdStat[] + transactions Transaction[] + + @@map("users") +} + +model Skin { + uuid String @id + displayName String? + contentTierUuid String? + displayIcon String? + userId String? @map("user_id") + tierRank String? + tierColor String? + tierText String? + basePrice String? + currentLvl Int? + currentChroma Int? + currentPrice Int? + maxPrice Int? + + owner User? @relation(fields: [userId], references: [id]) + marketOffers MarketOffer[] + + @@map("skins") +} + +model MarketOffer { + id String @id + skinUuid String @map("skin_uuid") + sellerId String @map("seller_id") + startingPrice Int @map("starting_price") + buyoutPrice Int? @map("buyout_price") + finalPrice Int? @map("final_price") + status String + postedAt DateTime? @default(now()) @map("posted_at") + openingAt DateTime @map("opening_at") + closingAt DateTime @map("closing_at") + buyerId String? @map("buyer_id") + + skin Skin @relation(fields: [skinUuid], references: [uuid]) + seller User @relation("Seller", fields: [sellerId], references: [id]) + buyer User? @relation("Buyer", fields: [buyerId], references: [id]) + bids Bid[] + + @@map("market_offers") +} + +model Bid { + id String @id + bidderId String @map("bidder_id") + marketOfferId String @map("market_offer_id") + offerAmount Int @map("offer_amount") + offeredAt DateTime? @default(now()) @map("offered_at") + + bidder User @relation(fields: [bidderId], references: [id]) + marketOffer MarketOffer @relation(fields: [marketOfferId], references: [id]) + + @@map("bids") +} + +model Log { + id String @id + userId String @map("user_id") + action String? + targetUserId String? @map("target_user_id") + coinsAmount Int? @map("coins_amount") + userNewAmount Int? @map("user_new_amount") + createdAt DateTime? @default(now()) @map("created_at") + + user User @relation("UserLogs", fields: [userId], references: [id]) + targetUser User? @relation("TargetUserLogs", fields: [targetUserId], references: [id]) + + @@map("logs") +} + +model Game { + id String @id + p1 String + p2 String? + p1Score Int? @map("p1_score") + p2Score Int? @map("p2_score") + p1Elo Int? @map("p1_elo") + p2Elo Int? @map("p2_elo") + p1NewElo Int? @map("p1_new_elo") + p2NewElo Int? @map("p2_new_elo") + type String? + timestamp DateTime? + + player1 User @relation("Player1", fields: [p1], references: [id]) + player2 User? @relation("Player2", fields: [p2], references: [id]) + + @@map("games") +} + +model Elo { + id String @id + elo Int + + user User @relation(fields: [id], references: [id]) + + @@map("elos") +} + +model Sotd { + id Int @id + tableauPiles String? + foundationPiles String? + stockPile String? + wastePile String? + isDone Int @default(0) + seed String? + + @@map("sotd") +} + +model SotdStat { + id String @id + userId String @map("user_id") + time Int? + moves Int? + score Int? + + user User @relation(fields: [userId], references: [id]) + + @@map("sotd_stats") +} + +model Transaction { + id String @id + sessionId String @unique @map("session_id") + userId String @map("user_id") + coinsAmount Int @map("coins_amount") + amountCents Int @map("amount_cents") + currency String @default("eur") + customerEmail String? @map("customer_email") + customerName String? @map("customer_name") + paymentStatus String @map("payment_status") + createdAt DateTime? @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id]) + + @@map("transactions") +} diff --git a/src/bot/commands/inventory.js b/src/bot/commands/inventory.js index 4f37f99..0cb61d8 100644 --- a/src/bot/commands/inventory.js +++ b/src/bot/commands/inventory.js @@ -5,7 +5,7 @@ import { InteractionResponseFlags, } from "discord-interactions"; import { activeInventories, skins } from "../../game/state.js"; -import { getUserInventory } from "../../database/index.js"; +import * as skinService from "../../services/skin.service.js"; /** * Handles the /inventory slash command. @@ -33,7 +33,7 @@ export async function handleInventoryCommand(req, res, client, interactionId) { // --- 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 }); + const inventorySkins = await skinService.getUserInventory(targetUserId); // --- 2. Handle Empty Inventory --- if (inventorySkins.length === 0) { diff --git a/src/bot/commands/search.js b/src/bot/commands/search.js index e9a4b84..ab9fe11 100644 --- a/src/bot/commands/search.js +++ b/src/bot/commands/search.js @@ -5,7 +5,7 @@ import { ButtonStyleTypes, } from "discord-interactions"; import { activeSearchs, skins } from "../../game/state.js"; -import { getAllSkins } from "../../database/index.js"; +import * as skinService from "../../services/skin.service.js"; /** * Handles the /search slash command. @@ -23,7 +23,7 @@ export async function handleSearchCommand(req, res, client, interactionId) { try { // --- 1. Fetch and Filter Data --- - const allDbSkins = getAllSkins.all(); + const allDbSkins = await skinService.getAllSkins(); const resultSkins = allDbSkins.filter( (skin) => skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue), @@ -61,12 +61,12 @@ export async function handleSearchCommand(req, res, client, interactionId) { // Fetch owner details if the skin is owned let ownerText = ""; - if (currentSkin.user_id) { + if (currentSkin.userId) { try { - const owner = await guild.members.fetch(currentSkin.user_id); + const owner = await guild.members.fetch(currentSkin.userId); ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`; } catch (e) { - console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); + console.warn(`Could not fetch owner for user ID: ${currentSkin.userId}`); ownerText = "| Appartenant à un utilisateur inconnu"; } } diff --git a/src/bot/commands/skins.js b/src/bot/commands/skins.js index 5b568c6..5d221e5 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 * as skinService from "../../services/skin.service.js"; /** * Handles the /skins slash command. @@ -13,7 +13,7 @@ export async function handleSkinsCommand(req, res, client) { try { // --- 1. Fetch Data --- - const topSkins = getTopSkins.all(); + const topSkins = await skinService.getTopSkins(); const guild = await client.guilds.fetch(guild_id); const fields = []; @@ -23,14 +23,14 @@ export async function handleSkinsCommand(req, res, client) { let ownerText = "Libre"; // Default text if the skin has no owner // If the skin has an owner, fetch their details - if (skin.user_id) { + if (skin.userId) { try { - const owner = await guild.members.fetch(skin.user_id); + const owner = await guild.members.fetch(skin.userId); // 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}`); + console.warn(`Could not fetch owner for user ID: ${skin.userId}`); ownerText = "Appartient à un utilisateur inconnu"; } } diff --git a/src/bot/commands/timeout.js b/src/bot/commands/timeout.js index f9ad3ef..a1539b2 100644 --- a/src/bot/commands/timeout.js +++ b/src/bot/commands/timeout.js @@ -9,7 +9,7 @@ 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 * as userService from "../../services/user.service.js"; /** * Handles the /timeout slash command. @@ -102,12 +102,12 @@ export async function handleTimeoutCommand(req, res, client) { if (remaining === 0) { clearInterval(countdownInterval); - const votersList = poll.voters - .map((voterId) => { - const user = getUser.get(voterId); + const votersList = (await Promise.all(poll.voters + .map(async (voterId) => { + const user = await userService.getUser(voterId); return `- ${user?.globalName || "Utilisateur Inconnu"}`; }) - .join("\n"); + )).join("\n"); try { await DiscordRequest(poll.endpoint, { @@ -143,12 +143,12 @@ export async function handleTimeoutCommand(req, res, client) { // --- Periodic Update Logic --- // Update the message every second with the new countdown try { - const votersList = poll.voters - .map((voterId) => { - const user = getUser.get(voterId); + const votersList = (await Promise.all(poll.voters + .map(async (voterId) => { + const user = await userService.getUser(voterId); return `- ${user?.globalName || "Utilisateur Inconnu"}`; }) - .join("\n"); + )).join("\n"); await DiscordRequest(poll.endpoint, { method: "PATCH", diff --git a/src/bot/commands/valorant.js b/src/bot/commands/valorant.js index de25bef..37c4f61 100644 --- a/src/bot/commands/valorant.js +++ b/src/bot/commands/valorant.js @@ -1,7 +1,9 @@ import { InteractionResponseFlags, InteractionResponseType } from "discord-interactions"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { DiscordRequest } from "../../api/discord.js"; -import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js"; +import * as userService from "../../services/user.service.js"; +import * as skinService from "../../services/skin.service.js"; +import * as logService from "../../services/log.service.js"; import { skins } from "../../game/state.js"; /** @@ -27,7 +29,7 @@ export async function handleValorantCommand(req, res, client) { try { // --- 1. Verify and process payment --- - const commandUser = getUser.get(userId); + const commandUser = await userService.getUser(userId); if (!commandUser) { return res.send({ type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, @@ -47,18 +49,15 @@ export async function handleValorantCommand(req, res, client) { }); } - insertLog.run({ + await logService.insertLog({ id: `${userId}-${Date.now()}`, - user_id: userId, + userId: 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, + targetUserId: null, + coinsAmount: -valoPrice, + userNewAmount: commandUser.coins - valoPrice, }); + await userService.updateUserCoins(userId, commandUser.coins - valoPrice); // --- 2. Send Initial "Opening" Response --- // Acknowledge the interaction immediately with a loading message. @@ -77,7 +76,7 @@ export async function handleValorantCommand(req, res, client) { const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; try { // --- Skin Selection --- - const availableSkins = getAllAvailableSkins.all(); + const availableSkins = await skinService.getAllAvailableSkins(); if (availableSkins.length === 0) { throw new Error("No available skins to award."); } @@ -105,9 +104,9 @@ export async function handleValorantCommand(req, res, client) { const finalPrice = calculatePrice(); // --- Update Database --- - await updateSkin.run({ + await skinService.updateSkin({ uuid: randomSkinData.uuid, - user_id: userId, + userId: userId, currentLvl: randomLevel, currentChroma: randomChroma, currentPrice: finalPrice, diff --git a/src/bot/components/pollVote.js b/src/bot/components/pollVote.js index 12bef1a..457626d 100644 --- a/src/bot/components/pollVote.js +++ b/src/bot/components/pollVote.js @@ -2,7 +2,7 @@ import { InteractionResponseType, InteractionResponseFlags } from "discord-inter 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 * as userService from "../../services/user.service.js"; /** * Handles clicks on the 'Yes' or 'No' buttons of a timeout poll. @@ -75,7 +75,10 @@ export async function handlePollVote(req, res) { 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 = (await Promise.all(poll.voters.map(async (vId) => { + const user = await userService.getUser(vId); + return `- ${user?.globalName || "Utilisateur Inconnu"}`; + }))).join("\n"); // --- 4. Check for Majority --- if (isVotingFor && poll.for >= poll.requiredMajority) { diff --git a/src/bot/components/searchNav.js b/src/bot/components/searchNav.js index afb71e7..01d371f 100644 --- a/src/bot/components/searchNav.js +++ b/src/bot/components/searchNav.js @@ -65,12 +65,12 @@ export async function handleSearchNav(req, res, client) { // Fetch owner details if the skin is owned let ownerText = ""; - if (currentSkin.user_id) { + if (currentSkin.userId) { try { - const owner = await client.users.fetch(currentSkin.user_id); + const owner = await client.users.fetch(currentSkin.userId); ownerText = `| **@${owner.globalName || owner.username}** ✅`; } catch (e) { - console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); + console.warn(`Could not fetch owner for user ID: ${currentSkin.userId}`); ownerText = "| Appartenant à un utilisateur inconnu"; } } diff --git a/src/bot/components/upgradeSkin.js b/src/bot/components/upgradeSkin.js index ff03cf0..a9cb6ed 100644 --- a/src/bot/components/upgradeSkin.js +++ b/src/bot/components/upgradeSkin.js @@ -9,7 +9,9 @@ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "disc 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 * as userService from "../../services/user.service.js"; +import * as skinService from "../../services/skin.service.js"; +import * as logService from "../../services/log.service.js"; /** * Handles the click of the 'Upgrade' button on a skin in the inventory. @@ -65,7 +67,7 @@ export async function handleUpgradeSkin(req, res) { // --- 2. Handle Payment --- const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10; - const commandUser = getUser.get(userId); + const commandUser = await userService.getUser(userId); if (!commandUser) { return res.send({ @@ -86,18 +88,15 @@ export async function handleUpgradeSkin(req, res) { }); } - insertLog.run({ + await logService.insertLog({ id: `${userId}-${Date.now()}`, - user_id: userId, + userId: 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), + targetUserId: null, + coinsAmount: -upgradePrice.toFixed(0), + userNewAmount: commandUser.coins - upgradePrice.toFixed(0), }); + await userService.updateUserCoins(userId, commandUser.coins - upgradePrice.toFixed(0)); // --- 3. Show Loading Animation --- // Acknowledge the click immediately and then edit the message to show a loading state. @@ -151,9 +150,9 @@ export async function handleUpgradeSkin(req, res) { }; skinToUpgrade.currentPrice = calculatePrice(); - await updateSkin.run({ + await skinService.updateSkin({ uuid: skinToUpgrade.uuid, - user_id: skinToUpgrade.user_id, + userId: skinToUpgrade.userId, currentLvl: skinToUpgrade.currentLvl, currentChroma: skinToUpgrade.currentChroma, currentPrice: skinToUpgrade.currentPrice, @@ -165,7 +164,7 @@ export async function handleUpgradeSkin(req, res) { // --- 6. Send Final Result --- setTimeout(async () => { // Fetch the latest state of the skin from the database - const finalSkinState = getSkin.get(skinToUpgrade.uuid); + const finalSkinState = await skinService.getSkin(skinToUpgrade.uuid); const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData); const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId); diff --git a/src/bot/handlers/messageCreate.js b/src/bot/handlers/messageCreate.js index 0fe47ac..61cde4d 100644 --- a/src/bot/handlers/messageCreate.js +++ b/src/bot/handlers/messageCreate.js @@ -13,18 +13,10 @@ import { import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys } from "../../utils/index.js"; import { channelPointsHandler, initTodaysSOTD, randomSkinPrice, slowmodesHandler } from "../../game/points.js"; import { activePolls, activeSlowmodes, requestTimestamps, skins } from "../../game/state.js"; -import { - flopoDB, - getAllSkins, - getAllUsers, - getUser, - hardUpdateSkin, - insertLog, - updateManyUsers, - updateSkin, - updateUserAvatar, - updateUserCoins, -} from "../../database/index.js"; +import prisma from "../../prisma/client.js"; +import * as userService from "../../services/user.service.js"; +import * as skinService from "../../services/skin.service.js"; +import * as logService from "../../services/log.service.js"; import { client } from "../client.js"; import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js"; @@ -89,7 +81,7 @@ export async function handleMessageCreate(message, client, io) { // --- Sub-handler for AI Logic --- async function handleAiMention(message, client, io) { const authorId = message.author.id; - let authorDB = getUser.get(authorId); + let authorDB = await userService.getUser(authorId); if (!authorDB) return; // Should not happen if user is in DB, but good practice // --- Rate Limiting --- @@ -105,7 +97,7 @@ async function handleAiMention(message, client, io) { authorDB.warned = 1; authorDB.warns += 1; authorDB.allTimeWarns += 1; - updateManyUsers([authorDB]); + await userService.updateManyUsers([authorDB]); // Apply timeout if warn count is too high if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) { @@ -135,7 +127,7 @@ async function handleAiMention(message, client, io) { authorDB.warned = 0; authorDB.warns = 0; authorDB.totalRequests += 1; - updateManyUsers([authorDB]); + await userService.updateManyUsers([authorDB]); // --- AI Processing --- try { @@ -239,13 +231,14 @@ async function handleAdminCommands(message) { message.reply("New Solitaire of the Day initialized."); break; case `${prefix}:users`: - console.log(getAllUsers.all()); + console.log(await userService.getAllUsers()); 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(); + const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") + ? await prisma.$queryRawUnsafe(sqlCommand) + : await prisma.$executeRawUnsafe(sqlCommand); const jsonString = JSON.stringify(result, null, 2); const buffer = Buffer.from(jsonString, "utf-8"); const attachment = new AttachmentBuilder(buffer, { name: "sql-result.json" }); @@ -267,16 +260,16 @@ async function handleAdminCommands(message) { avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }), })); - usersToUpdate.forEach((user) => { + for (const user of usersToUpdate) { try { - updateUserAvatar.run(user); + await userService.updateUserAvatar(user.id, user.avatarUrl); } catch (err) {} - }); + } break; case `${prefix}:rework-skins`: console.log("Reworking all skin prices..."); - const dbSkins = getAllSkins.all(); - dbSkins.forEach((skin) => { + const dbSkins = await skinService.getAllSkins(); + for (const skin of dbSkins) { const fetchedSkin = skins.find((s) => s.uuid === skin.uuid); const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0); const calculatePrice = () => { @@ -287,12 +280,12 @@ async function handleAdminCommands(message) { return parseFloat(result.toFixed(0)); }; const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0); - hardUpdateSkin.run({ + await skinService.hardUpdateSkin({ uuid: skin.uuid, displayName: skin.displayName, contentTierUuid: skin.contentTierUuid, displayIcon: skin.displayIcon, - user_id: skin.user_id, + userId: skin.userId, tierRank: skin.tierRank, tierColor: skin.tierColor, tierText: skin.tierText, @@ -302,7 +295,7 @@ async function handleAdminCommands(message) { currentPrice: skin.currentPrice ? calculatePrice() : null, maxPrice: maxPrice, }); - }); + } console.log("Reworked", dbSkins.length, "skins."); break; case `${prefix}:cases-test`: @@ -328,7 +321,7 @@ async function handleAdminCommands(message) { for (let i = 0; i < caseCount; i++) { const skins = await drawCaseContent(caseType); - const result = drawCaseSkin(skins); + const result = await drawCaseSkin(skins); totalResValue += result.finalPrice; if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice; if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1; @@ -358,26 +351,23 @@ async function handleAdminCommands(message) { break; case `${prefix}:refund-skins`: try { - const DBskins = getAllSkins.all(); + const DBskins = await skinService.getAllSkins(); for (const skin of DBskins) { - const owner = getUser.get(skin.user_id); + const owner = await userService.getUser(skin.userId); if (owner) { - updateUserCoins.run({ - id: owner.id, - coins: owner.coins + skin.currentPrice, - }); - insertLog.run({ + await userService.updateUserCoins(owner.id, owner.coins + skin.currentPrice); + await logService.insertLog({ id: `${skin.uuid}-skin-refund-${Date.now()}`, - user_id: owner.id, - target_user_id: null, + userId: owner.id, + targetUserId: null, action: "SKIN_REFUND", - coins_amount: skin.currentPrice, - user_new_amount: owner.coins + skin.currentPrice, + coinsAmount: skin.currentPrice, + userNewAmount: owner.coins + skin.currentPrice, }); } - updateSkin.run({ + await skinService.updateSkin({ uuid: skin.uuid, - user_id: null, + userId: null, currentPrice: null, currentLvl: null, currentChroma: null, diff --git a/src/database/index.js b/src/database/index.js deleted file mode 100644 index a3b96f5..0000000 --- a/src/database/index.js +++ /dev/null @@ -1,972 +0,0 @@ -import Database from "better-sqlite3"; - -export const flopoDB = new Database(process.env.DB_PATH || "flopobot.db"); - -/* ------------------------- - CREATE ALL TABLES FIRST -----------------------------*/ -flopoDB.exec(` - 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 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 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 - ); - - 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 - ); - - 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, - created_at - TIMESTAMP - DEFAULT - CURRENT_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 - ); - - CREATE TABLE IF NOT EXISTS elos - ( - id - PRIMARY - KEY - REFERENCES - users, - elo - INTEGER - ); - - 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_stats - ( - id - TEXT - PRIMARY - KEY, - user_id - TEXT - REFERENCES - users, - time - INTEGER, - moves - INTEGER, - score - INTEGER - ); - - CREATE TABLE IF NOT EXISTS transactions - ( - id - TEXT - PRIMARY - KEY, - session_id - TEXT - UNIQUE - NOT - NULL, - user_id - TEXT - REFERENCES - users - NOT - NULL, - coins_amount - INTEGER - NOT - NULL, - amount_cents - INTEGER - NOT - NULL, - currency - TEXT - DEFAULT - 'eur', - customer_email - TEXT, - customer_name - TEXT, - payment_status - TEXT - NOT - NULL, - created_at - DATETIME - DEFAULT - CURRENT_TIMESTAMP - ); -`); - -/* ----------------------------------------------------- - PREPARE ANY CREATE TABLE STATEMENT OBJECTS (kept for parity) -------------------------------------------------------*/ - -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 - ) -`); -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 - ) -`); -stmtSkins.run(); - -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 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, - created_at - TIMESTAMP - DEFAULT - CURRENT_TIMESTAMP - ) -`); -stmtLogs.run(); - -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 - ) -`); -stmtGames.run(); - -export const stmtElos = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS elos - ( - id - PRIMARY - KEY - REFERENCES - users, - elo - INTEGER - ) -`); -stmtElos.run(); - -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 - ) -`); -stmtSOTD.run(); - -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 - ) -`); -stmtSOTDStats.run(); - -export const stmtTransactions = flopoDB.prepare(` - CREATE TABLE IF NOT EXISTS transactions - ( - id - TEXT - PRIMARY - KEY, - session_id - TEXT - UNIQUE - NOT - NULL, - user_id - TEXT - REFERENCES - users - NOT - NULL, - coins_amount - INTEGER - NOT - NULL, - amount_cents - INTEGER - NOT - NULL, - currency - TEXT - DEFAULT - 'eur', - customer_email - TEXT, - customer_name - TEXT, - payment_status - TEXT - NOT - NULL, - created_at - DATETIME - DEFAULT - CURRENT_TIMESTAMP - ) -`); -stmtTransactions.run(); - -/* ------------------------- - USER statements -----------------------------*/ -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", -); - -/* ------------------------- - SKINS statements -----------------------------*/ -export const insertSkin = flopoDB.prepare( - `INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, - basePrice, maxPrice) - VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, - @basePrice, @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"); - -/* ------------------------- - MARKET / BIDS / OFFERS -----------------------------*/ -export const getMarketOffers = flopoDB.prepare(` - SELECT * - FROM market_offers - 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 getMarketOffersBySkin = 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.skin_uuid = ? -`); -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 updateMarketOffer = flopoDB.prepare(` - UPDATE market_offers - SET final_price = @final_price, - status = @status, - buyer_id = @buyer_id - WHERE id = @id -`); -export const deleteMarketOffer = flopoDB.prepare(` - DELETE - FROM market_offers - WHERE id = ? -`); - -/* ------------------------- - BIDS -----------------------------*/ -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 deleteBid = flopoDB.prepare(` - DELETE - FROM bids - WHERE id = ? -`); - -/* ------------------------- - BULK TRANSACTIONS (synchronous) -----------------------------*/ -export const insertManyUsers = flopoDB.transaction((users) => { - for (const user of users) - try { - insertUser.run(user); - } catch (e) {} -}); -export const updateManyUsers = flopoDB.transaction((users) => { - for (const user of users) - try { - updateUser.run(user); - } catch (e) { - console.log(`User update failed`); - } -}); -export const insertManySkins = flopoDB.transaction((skins) => { - for (const skin of skins) - try { - insertSkin.run(skin); - } catch (e) {} -}); -export const updateManySkins = flopoDB.transaction((skins) => { - for (const skin of skins) - try { - updateSkin.run(skin); - } catch (e) {} -}); - -/* ------------------------- - LOGS -----------------------------*/ -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"); - -/* ------------------------- - GAMES -----------------------------*/ -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", -); - -/* ------------------------- - ELOS -----------------------------*/ -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", -); - -/* ------------------------- - SOTD -----------------------------*/ -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 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 = ?`); - -/* ------------------------- - pruneOldLogs -----------------------------*/ -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 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 created_at DESC) AS rn - FROM logs - WHERE user_id = ?) - WHERE rn > ${process.env.LOGS_BY_USER}) - `, - ) - .run(user_id); - } - }); - - transaction(); -} - -/* ------------------------- - TRANSACTION statements -----------------------------*/ -export const insertTransaction = flopoDB.prepare( - `INSERT INTO transactions (id, session_id, user_id, coins_amount, amount_cents, currency, customer_email, customer_name, payment_status) - VALUES (@id, @session_id, @user_id, @coins_amount, @amount_cents, @currency, @customer_email, @customer_name, @payment_status)`, -); - -export const getTransactionBySessionId = flopoDB.prepare( - `SELECT * FROM transactions WHERE session_id = ?`, -); - -export const getAllTransactions = flopoDB.prepare( - `SELECT * FROM transactions ORDER BY created_at DESC`, -); - -export const getUserTransactions = flopoDB.prepare( - `SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC`, -); diff --git a/src/game/blackjack.js b/src/game/blackjack.js index 501fb7a..81c8083 100644 --- a/src/game/blackjack.js +++ b/src/game/blackjack.js @@ -3,7 +3,8 @@ // Inspired by your poker helpers API style. import { emitToast } from "../server/socket.js"; -import { getUser, insertLog, updateUserCoins } from "../database/index.js"; +import * as userService from "../services/user.service.js"; +import * as logService from "../services/log.service.js"; import { client } from "../bot/client.js"; import { EmbedBuilder } from "discord.js"; @@ -299,21 +300,18 @@ export async function settleAll(room) { p.totalDelta += res.delta; p.totalBets++; if (res.result === "win" || res.result === "push" || res.result === "blackjack") { - const userDB = getUser.get(p.id); + const userDB = await userService.getUser(p.id); if (userDB) { const coins = userDB.coins; try { - updateUserCoins.run({ - id: p.id, - coins: coins + hand.bet + res.delta, - }); - insertLog.run({ + await userService.updateUserCoins(p.id, coins + hand.bet + res.delta); + await logService.insertLog({ id: `${p.id}-blackjack-${Date.now()}`, - user_id: p.id, - target_user_id: null, + userId: p.id, + targetUserId: null, action: "BLACKJACK_PAYOUT", - coins_amount: res.delta + hand.bet, - user_new_amount: coins + hand.bet + res.delta, + coinsAmount: res.delta + hand.bet, + userNewAmount: coins + hand.bet + res.delta, }); p.bank = coins + hand.bet + res.delta; } catch (e) { diff --git a/src/game/elo.js b/src/game/elo.js index 1032337..f69f37c 100644 --- a/src/game/elo.js +++ b/src/game/elo.js @@ -1,4 +1,5 @@ -import { getUser, getUserElo, insertElos, insertGame, updateElo } from "../database/index.js"; +import * as userService from "../services/user.service.js"; +import * as gameService from "../services/game.service.js"; import { ButtonStyle, EmbedBuilder } from "discord.js"; import { client } from "../bot/client.js"; @@ -12,23 +13,23 @@ import { client } from "../bot/client.js"; */ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = null) { // --- 1. Fetch Player Data --- - const p1DB = getUser.get(p1Id); - const p2DB = getUser.get(p2Id); + const p1DB = await userService.getUser(p1Id); + const p2DB = await userService.getUser(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 = await gameService.getUserElo(p1Id); + let p2EloData = await gameService.getUserElo(p2Id); // --- 2. Initialize Elo if it doesn't exist --- if (!p1EloData) { - await insertElos.run({ id: p1Id, elo: 1000 }); + await gameService.insertElo(p1Id, 1000); p1EloData = { id: p1Id, elo: 1000 }; } if (!p2EloData) { - await insertElos.run({ id: p2Id, elo: 1000 }); + await gameService.insertElo(p2Id, 1000); p2EloData = { id: p2Id, elo: 1000 }; } @@ -91,34 +92,34 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = nu } // --- 4. Update Database --- - updateElo.run({ id: p1Id, elo: finalP1Elo }); - updateElo.run({ id: p2Id, elo: finalP2Elo }); + await gameService.updateElo(p1Id, finalP1Elo); + await gameService.updateElo(p2Id, finalP2Elo); if (scores) { - insertGame.run({ + await gameService.insertGame({ id: `${p1Id}-${p2Id}-${Date.now()}`, p1: p1Id, p2: p2Id, - p1_score: scores.p1, - p2_score: scores.p2, - p1_elo: p1CurrentElo, - p2_elo: p2CurrentElo, - p1_new_elo: finalP1Elo, - p2_new_elo: finalP2Elo, + p1Score: scores.p1, + p2Score: scores.p2, + p1Elo: p1CurrentElo, + p2Elo: p2CurrentElo, + p1NewElo: finalP1Elo, + p2NewElo: finalP2Elo, type: type, timestamp: Date.now(), }); } else { - insertGame.run({ + await gameService.insertGame({ 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, + p1Score: p1Score, + p2Score: p2Score, + p1Elo: p1CurrentElo, + p2Elo: p2CurrentElo, + p1NewElo: finalP1Elo, + p2NewElo: finalP2Elo, type: type, timestamp: Date.now(), }); @@ -141,11 +142,12 @@ export async function pokerEloHandler(room) { 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; + const dbPlayers = await Promise.all(playerIds.map(async (id) => { + const user = await userService.getUser(id); + const eloData = await gameService.getUserElo(id); + const elo = eloData?.elo || 1000; return { ...user, elo }; - }); + })); const winnerIds = new Set(room.winners); const playerCount = dbPlayers.length; @@ -153,7 +155,7 @@ export async function pokerEloHandler(room) { const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount; - dbPlayers.forEach((player) => { + for (const player of dbPlayers) { // 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)); @@ -175,23 +177,23 @@ export async function pokerEloHandler(room) { console.log( `Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`, ); - updateElo.run({ id: player.id, elo: newElo }); + await gameService.updateElo(player.id, newElo); - insertGame.run({ + await gameService.insertGame({ 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, + p1Score: actualScore, + p2Score: null, + p1Elo: player.elo, + p2Elo: Math.round(averageElo), // Log the average opponent Elo for context + p1NewElo: newElo, + p2NewElo: 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 8ae58b9..7ac391a 100644 --- a/src/game/points.js +++ b/src/game/points.js @@ -1,15 +1,7 @@ -import { - clearSOTDStats, - deleteSOTD, - getAllSkins, - getAllSOTDStats, - getUser, - insertGame, - insertLog, - insertSOTD, - pruneOldLogs, - updateUserCoins -} from "../database/index.js"; +import * as userService from "../services/user.service.js"; +import * as skinService from "../services/skin.service.js"; +import * as logService from "../services/log.service.js"; +import * as solitaireService from "../services/solitaire.service.js"; import { activeSlowmodes, activeSolitaireGames, messagesTimestamps, skins } from "./state.js"; import { createDeck, createSeededRNG, deal, seededShuffle } from "./solitaire.js"; import { emitSolitaireUpdate } from "../server/socket.js"; @@ -22,7 +14,7 @@ import { emitSolitaireUpdate } from "../server/socket.js"; */ export async function channelPointsHandler(message) { const author = message.author; - const authorDB = getUser.get(author.id); + const authorDB = await userService.getUser(author.id); if (!authorDB) { // User not in our database, do nothing. @@ -53,21 +45,18 @@ export async function channelPointsHandler(message) { const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10; const newCoinTotal = authorDB.coins + coinsToAdd; - updateUserCoins.run({ - id: author.id, - coins: newCoinTotal, - }); + await userService.updateUserCoins(author.id, newCoinTotal); - insertLog.run({ + await logService.insertLog({ id: `${author.id}-${now}`, - user_id: author.id, + userId: author.id, action: "AUTO_COINS", - target_user_id: null, - coins_amount: coinsToAdd, - user_new_amount: newCoinTotal, + targetUserId: null, + coinsAmount: coinsToAdd, + userNewAmount: newCoinTotal, }); - await pruneOldLogs(); + await logService.pruneOldLogs(); return true; // Indicate that points were awarded } @@ -116,8 +105,8 @@ export async function slowmodesHandler(message) { * Used for testing and simulations. * @returns {string} The calculated random price as a string. */ -export function randomSkinPrice() { - const dbSkins = getAllSkins.all(); +export async function randomSkinPrice() { + const dbSkins = await skinService.getAllSkins(); if (dbSkins.length === 0) return "0.00"; const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)]; @@ -144,30 +133,30 @@ export function randomSkinPrice() { * Initializes the Solitaire of the Day. * This function clears previous stats, awards the winner, and generates a new daily seed. */ -export function initTodaysSOTD() { +export async function initTodaysSOTD() { console.log(`Initializing new Solitaire of the Day...`); // 1. Award previous day's winner - const rankings = getAllSOTDStats.all(); + const rankings = await solitaireService.getAllSOTDStats(); if (rankings.length > 0) { - const winnerId = rankings[0].user_id; - const secondPlaceId = rankings[1] ? rankings[1].user_id : null; - const thirdPlaceId = rankings[2] ? rankings[2].user_id : null; - const winnerUser = getUser.get(winnerId); - const secondPlaceUser = secondPlaceId ? getUser.get(secondPlaceId) : null; - const thirdPlaceUser = thirdPlaceId ? getUser.get(thirdPlaceId) : null; + const winnerId = rankings[0].userId; + const secondPlaceId = rankings[1] ? rankings[1].userId : null; + const thirdPlaceId = rankings[2] ? rankings[2].userId : null; + const winnerUser = await userService.getUser(winnerId); + const secondPlaceUser = secondPlaceId ? await userService.getUser(secondPlaceId) : null; + const thirdPlaceUser = thirdPlaceId ? await userService.getUser(thirdPlaceId) : null; if (winnerUser) { const reward = 2500; const newCoinTotal = winnerUser.coins + reward; - updateUserCoins.run({ id: winnerId, coins: newCoinTotal }); - insertLog.run({ + await userService.updateUserCoins(winnerId, newCoinTotal); + await logService.insertLog({ id: `${winnerId}-sotd-win-${Date.now()}`, - target_user_id: null, - user_id: winnerId, + targetUserId: null, + userId: winnerId, action: "SOTD_FIRST_PLACE", - coins_amount: reward, - user_new_amount: newCoinTotal, + coinsAmount: reward, + userNewAmount: newCoinTotal, }); console.log( `${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`, @@ -176,14 +165,14 @@ export function initTodaysSOTD() { if (secondPlaceUser) { const reward = 1500; const newCoinTotal = secondPlaceUser.coins + reward; - updateUserCoins.run({ id: secondPlaceId, coins: newCoinTotal }); - insertLog.run({ + await userService.updateUserCoins(secondPlaceId, newCoinTotal); + await logService.insertLog({ id: `${secondPlaceId}-sotd-second-${Date.now()}`, - target_user_id: null, - user_id: secondPlaceId, + targetUserId: null, + userId: secondPlaceId, action: "SOTD_SECOND_PLACE", - coins_amount: reward, - user_new_amount: newCoinTotal, + coinsAmount: reward, + userNewAmount: newCoinTotal, }); console.log( `${secondPlaceUser.globalName || secondPlaceUser.username} got second place in the previous SOTD and received ${reward} coins.`, @@ -192,14 +181,14 @@ export function initTodaysSOTD() { if (thirdPlaceUser) { const reward = 750; const newCoinTotal = thirdPlaceUser.coins + reward; - updateUserCoins.run({ id: thirdPlaceId, coins: newCoinTotal }); - insertLog.run({ + await userService.updateUserCoins(thirdPlaceId, newCoinTotal); + await logService.insertLog({ id: `${thirdPlaceId}-sotd-third-${Date.now()}`, - target_user_id: null, - user_id: thirdPlaceId, + targetUserId: null, + userId: thirdPlaceId, action: "SOTD_THIRD_PLACE", - coins_amount: reward, - user_new_amount: newCoinTotal, + coinsAmount: reward, + userNewAmount: newCoinTotal, }); console.log( `${thirdPlaceUser.globalName || thirdPlaceUser.username} got third place in the previous SOTD and received ${reward} coins.`, @@ -221,9 +210,9 @@ export function initTodaysSOTD() { // 3. Clear old stats and save the new game state to the database try { - clearSOTDStats.run(); - deleteSOTD.run(); - insertSOTD.run({ + await solitaireService.clearSOTDStats(); + await solitaireService.deleteSOTD(); + await solitaireService.insertSOTD({ tableauPiles: JSON.stringify(todaysSOTD.tableauPiles), foundationPiles: JSON.stringify(todaysSOTD.foundationPiles), stockPile: JSON.stringify(todaysSOTD.stockPile), diff --git a/src/prisma/client.js b/src/prisma/client.js new file mode 100644 index 0000000..b5bf6ce --- /dev/null +++ b/src/prisma/client.js @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 0e97e0b..663e159 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -2,31 +2,13 @@ import express from "express"; import { sleep } from "openai/core"; import Stripe from "stripe"; -// --- Database Imports --- -import { - getAllAkhys, - getAllUsers, - getLogs, - getMarketOffersBySkin, - getOfferBids, - getSkin, - getUser, - getUserElo, - getUserGames, - getUserInventory, - getUserLogs, - getUsersByElo, - insertLog, - insertUser, - pruneOldLogs, - queryDailyReward, - updateSkin, - updateUserCoins, - insertTransaction, - getTransactionBySessionId, - getAllTransactions, - getUserTransactions, -} from "../../database/index.js"; +// --- Service Imports --- +import * as userService from "../../services/user.service.js"; +import * as gameService from "../../services/game.service.js"; +import * as skinService from "../../services/skin.service.js"; +import * as logService from "../../services/log.service.js"; +import * as transactionService from "../../services/transaction.service.js"; +import * as marketService from "../../services/market.service.js"; // --- Game State Imports --- import { activePolls, activePredis, activeSlowmodes, skins, activeSnakeGames } from "../../game/state.js"; @@ -57,9 +39,9 @@ export function apiRoutes(client, io) { res.status(200).json({ status: "OK", message: "FlopoBot API is running." }); }); - router.get("/users", (req, res) => { + router.get("/users", async (req, res) => { try { - const users = getAllUsers.all(); + const users = await userService.getAllUsers(); res.json(users); } catch (error) { console.error("Error fetching users:", error); @@ -67,9 +49,9 @@ export function apiRoutes(client, io) { } }); - router.get("/akhys", (req, res) => { + router.get("/akhys", async (req, res) => { try { - const akhys = getAllAkhys.all(); + const akhys = await userService.getAllAkhys(); res.json(akhys); } catch (error) { console.error("Error fetching akhys:", error); @@ -82,7 +64,7 @@ export function apiRoutes(client, io) { const discordUser = await client.users.fetch(discordUserId); try { - insertUser.run({ + await userService.insertUser({ id: discordUser.id, username: discordUser.username, globalName: discordUser.globalName, @@ -94,14 +76,14 @@ export function apiRoutes(client, io) { isAkhy: 0, }); - updateUserCoins.run({ id: discordUser.id, coins: 5000 }); - insertLog.run({ + await userService.updateUserCoins(discordUser.id, 5000); + await logService.insertLog({ id: `${discordUser.id}-welcome-${Date.now()}`, - user_id: discordUser.id, + userId: discordUser.id, action: "WELCOME_BONUS", - target_user_id: null, - coins_amount: 5000, - user_new_amount: 5000, + targetUserId: null, + coinsAmount: 5000, + userNewAmount: 5000, }); console.log(`New registered user: ${discordUser.username} (${discordUser.id})`); @@ -142,7 +124,7 @@ export function apiRoutes(client, io) { default: return res.status(400).json({ error: "Invalid case type." }); } - const commandUser = getUser.get(userId); + const commandUser = await userService.getUser(userId); if (!commandUser) return res.status(404).json({ error: "User not found." }); const valoPrice = caseTypeVal; if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." }); @@ -150,24 +132,21 @@ export function apiRoutes(client, io) { try { const selectedSkins = await drawCaseContent(caseType); - const result = drawCaseSkin(selectedSkins); + const result = await drawCaseSkin(selectedSkins); // --- Update Database --- - insertLog.run({ + await logService.insertLog({ id: `${userId}-${Date.now()}`, - user_id: userId, + userId: userId, action: "VALO_CASE_OPEN", - target_user_id: null, - coins_amount: -valoPrice, - user_new_amount: commandUser.coins - valoPrice, + targetUserId: null, + coinsAmount: -valoPrice, + userNewAmount: commandUser.coins - valoPrice, }); - updateUserCoins.run({ - id: userId, - coins: commandUser.coins - valoPrice, - }); - updateSkin.run({ + await userService.updateUserCoins(userId, commandUser.coins - valoPrice); + await skinService.updateSkin({ uuid: result.randomSkinData.uuid, - user_id: userId, + userId: userId, currentLvl: result.randomLevel, currentChroma: result.randomChroma, currentPrice: result.finalPrice, @@ -176,7 +155,7 @@ export function apiRoutes(client, io) { console.log( `${commandUser.username} opened a ${caseType} Valorant case and received skin ${result.randomSelectedSkinUuid}`, ); - const updatedSkin = getSkin.get(result.randomSkinData.uuid); + const updatedSkin = await skinService.getSkin(result.randomSkinData.uuid); await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client); const contentSkins = selectedSkins.map((item) => { @@ -204,14 +183,15 @@ export function apiRoutes(client, io) { const { type } = req.params; try { const selectedSkins = await drawCaseContent(type, -1); - selectedSkins.forEach((item) => { + for (const item of selectedSkins) { item.isMelee = isMeleeSkin(item.displayName); item.isVCT = isVCTSkin(item.displayName); item.isChampions = isChampionsSkin(item.displayName); item.vctRegion = getVCTRegion(item.displayName); - item.basePrice = getSkin.get(item.uuid).basePrice; - item.maxPrice = getSkin.get(item.uuid).maxPrice; - }); + const skinData = await skinService.getSkin(item.uuid); + item.basePrice = skinData.basePrice; + item.maxPrice = skinData.maxPrice; + } res.json({ skins: selectedSkins.sort((a, b) => b.maxPrice - a.maxPrice) }); } catch (error) { console.error("Error fetching case content:", error); @@ -251,47 +231,44 @@ export function apiRoutes(client, io) { } }); - router.post("/skin/:uuid/instant-sell", (req, res) => { + router.post("/skin/:uuid/instant-sell", async (req, res) => { const { userId } = req.body; try { - const skin = getSkin.get(req.params.uuid); + const skin = await skinService.getSkin(req.params.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid); if ( !skinData ) { return res.status(403).json({ error: "Invalid skin." }); } - if (skin.user_id !== userId) { + if (skin.userId !== userId) { return res.status(403).json({ error: "User does not own this skin." }); } - const marketOffers = getMarketOffersBySkin.all(skin.uuid); + const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid); const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open"); if (activeOffers.length > 0) { return res.status(403).json({ error: "Impossible de vendre ce skin, une offre FlopoMarket est déjà en cours." }); } - const commandUser = getUser.get(userId); + const commandUser = await userService.getUser(userId); if (!commandUser) { return res.status(404).json({ error: "User not found." }); } const sellPrice = skin.currentPrice; - insertLog.run({ + await logService.insertLog({ id: `${userId}-${Date.now()}`, - user_id: userId, + userId: userId, action: "VALO_SKIN_INSTANT_SELL", - target_user_id: null, - coins_amount: sellPrice, - user_new_amount: commandUser.coins + sellPrice, + targetUserId: null, + coinsAmount: sellPrice, + userNewAmount: commandUser.coins + sellPrice, }); - updateUserCoins.run({ - id: userId, - coins: commandUser.coins + sellPrice, - }); - updateSkin.run({ + await userService.updateUserCoins(userId, commandUser.coins + sellPrice); + await skinService.updateSkin({ uuid: skin.uuid, - user_id: null, + userId: null, currentLvl: null, currentChroma: null, currentPrice: null, @@ -304,9 +281,9 @@ export function apiRoutes(client, io) { } }); - router.get("/skin-upgrade/:uuid/fetch", (req, res) => { + router.get("/skin-upgrade/:uuid/fetch", async (req, res) => { try { - const skin = getSkin.get(req.params.uuid); + const skin = await skinService.getSkin(req.params.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid); const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData); @@ -326,7 +303,7 @@ export function apiRoutes(client, io) { router.post("/skin-upgrade/:uuid", async (req, res) => { const { userId } = req.body; try { - const skin = getSkin.get(req.params.uuid); + const skin = await skinService.getSkin(req.params.uuid); const skinData = skins.find((s) => s.uuid === skin.uuid); if ( !skinData || @@ -334,17 +311,17 @@ export function apiRoutes(client, io) { ) { return res.status(403).json({ error: "Skin is already maxed out or invalid skin." }); } - if (skin.user_id !== userId) { + if (skin.userId !== userId) { return res.status(403).json({ error: "User does not own this skin." }); } - const marketOffers = getMarketOffersBySkin.all(skin.uuid); + const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid); const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open"); if (activeOffers.length > 0) { return res.status(403).json({ error: "Impossible d'améliorer ce skin, une offre FlopoMarket est en cours." }); } const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData); - const commandUser = getUser.get(userId); + const commandUser = await userService.getUser(userId); if (!commandUser) { return res.status(404).json({ error: "User not found." }); } @@ -352,18 +329,15 @@ export function apiRoutes(client, io) { return res.status(403).json({ error: `Pas assez de FlopoCoins (${upgradePrice} requis).` }); } - insertLog.run({ + await logService.insertLog({ id: `${userId}-${Date.now()}`, - user_id: userId, + userId: userId, action: "VALO_SKIN_UPGRADE", - target_user_id: null, - coins_amount: -upgradePrice, - user_new_amount: commandUser.coins - upgradePrice, - }); - updateUserCoins.run({ - id: userId, - coins: commandUser.coins - upgradePrice, + targetUserId: null, + coinsAmount: -upgradePrice, + userNewAmount: commandUser.coins - upgradePrice, }); + await userService.updateUserCoins(userId, commandUser.coins - upgradePrice); let succeeded = false; let destructed = false; @@ -390,17 +364,17 @@ export function apiRoutes(client, io) { }; skin.currentPrice = calculatePrice(); - updateSkin.run({ + await skinService.updateSkin({ uuid: skin.uuid, - user_id: skin.user_id, + userId: skin.userId, currentLvl: skin.currentLvl, currentChroma: skin.currentChroma, currentPrice: skin.currentPrice, }); } else if (destructed) { - updateSkin.run({ + await skinService.updateSkin({ uuid: skin.uuid, - user_id: null, + userId: null, currentLvl: null, currentChroma: null, currentPrice: null, @@ -415,9 +389,9 @@ export function apiRoutes(client, io) { } }); - router.get("/users/by-elo", (req, res) => { + router.get("/users/by-elo", async (req, res) => { try { - const users = getUsersByElo.all(); + const users = await gameService.getUsersByElo(); res.json(users); } catch (error) { console.error("Error fetching users by Elo:", error); @@ -427,8 +401,8 @@ export function apiRoutes(client, io) { router.get("/logs", async (req, res) => { try { - await pruneOldLogs(); - const logs = getLogs.all(); + await logService.pruneOldLogs(); + const logs = await logService.getLogs(); res.status(200).json(logs); } catch (error) { console.error("Error fetching logs:", error); @@ -439,7 +413,7 @@ export function apiRoutes(client, io) { // --- User-Specific Routes --- router.get("/user/:id", async (req, res) => { try { - const user = getUser.get(req.params.id); + const user = await userService.getUser(req.params.id); res.json({ user }); } catch (error) { res.status(404).json({ error: "User not found." }); @@ -467,74 +441,75 @@ export function apiRoutes(client, io) { router.get("/user/:id/coins", async (req, res) => { try { - const user = getUser.get(req.params.id); + const user = await userService.getUser(req.params.id); res.json({ coins: user.coins }); } catch (error) { res.status(404).json({ error: "User not found." }); } }); - router.get("/user/:id/sparkline", (req, res) => { + router.get("/user/:id/sparkline", async (req, res) => { try { - const logs = getUserLogs.all({ user_id: req.params.id }); + const logs = await logService.getUserLogs(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) => { + router.get("/user/:id/elo", async (req, res) => { try { - const eloData = getUserElo.get({ id: req.params.id }); + const eloData = await gameService.getUserElo(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) => { + router.get("/user/:id/elo-graph", async (req, res) => { try { - const games = getUserGames.all({ user_id: req.params.id }); + const games = await gameService.getUserGames(req.params.id); const eloHistory = games .filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD') .filter((game) => game.p2 !== null) - .map((game) => (game.p1 === req.params.id ? game.p1_new_elo : game.p2_new_elo)); + .map((game) => (game.p1 === req.params.id ? game.p1NewElo : game.p2NewElo)); eloHistory.splice(0, 0, 1000); - res.json({ elo_graph: eloHistory }); + res.json({ eloGraph: eloHistory }); } catch (e) { res.status(500).json({ error: "Failed to generate Elo graph." }); } }); - router.get("/user/:id/inventory", (req, res) => { + router.get("/user/:id/inventory", async (req, res) => { try { - const inventory = getUserInventory.all({ user_id: req.params.id }); - inventory.forEach((skin) => { - const marketOffers = getMarketOffersBySkin.all(skin.uuid); - marketOffers.forEach((offer) => { - offer.skin = getSkin.get(offer.skin_uuid); - offer.seller = getUser.get(offer.seller_id); - offer.buyer = getUser.get(offer.buyer_id) || null; - offer.bids = getOfferBids.all(offer.id) || {}; - offer.bids.forEach((bid) => { - bid.bidder = getUser.get(bid.bidder_id); - }); - }); + const inventory = await skinService.getUserInventory(req.params.id); + for (const skin of inventory) { + const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid); + for (const offer of marketOffers) { + offer.skin = await skinService.getSkin(offer.skinUuid); + offer.seller = await userService.getUser(offer.sellerId); + offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null; + offer.bids = await marketService.getOfferBids(offer.id) || {}; + for (const bid of offer.bids) { + bid.bidder = await userService.getUser(bid.bidderId); + } + } skin.offers = marketOffers || {}; skin.isMelee = isMeleeSkin(skin.displayName); skin.isVCT = isVCTSkin(skin.displayName); skin.isChampions = isChampionsSkin(skin.displayName); skin.vctRegion = getVCTRegion(skin.displayName); - }); + } res.json({ inventory }); } catch (error) { + console.log(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 }).filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD').reverse().slice(0, 50); + const games = (await gameService.getUserGames(req.params.id)).filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD').reverse().slice(0, 50); res.json({ games }); } catch (err) { res.status(500).json({ error: "Failed to fetch games history." }); @@ -544,21 +519,21 @@ export function apiRoutes(client, io) { router.get("/user/:id/daily", async (req, res) => { const { id } = req.params; try { - const akhy = getUser.get(id); + const akhy = await userService.getUser(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({ + await userService.queryDailyReward(id); + await userService.updateUserCoins(id, newCoins); + await logService.insertLog({ id: `${id}-daily-${Date.now()}`, - user_id: id, + userId: id, action: "DAILY_REWARD", - target_user_id: null, - coins_amount: amount, - user_new_amount: newCoins, + targetUserId: null, + coinsAmount: amount, + userNewAmount: newCoins, }); await socketEmit("daily-queried", { userId: id }); @@ -589,7 +564,7 @@ export function apiRoutes(client, io) { router.post("/change-nickname", async (req, res) => { const { userId, nickname, commandUserId } = req.body; - const commandUser = getUser.get(commandUserId); + const commandUser = await userService.getUser(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)." }); @@ -600,14 +575,14 @@ export function apiRoutes(client, io) { await member.setNickname(nickname); const newCoins = commandUser.coins - 1000; - updateUserCoins.run({ id: commandUserId, coins: newCoins }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, newCoins); + await logService.insertLog({ id: `${commandUserId}-changenick-${Date.now()}`, - user_id: commandUserId, + userId: commandUserId, action: "CHANGE_NICKNAME", - target_user_id: userId, - coins_amount: -1000, - user_new_amount: newCoins, + targetUserId: userId, + coinsAmount: -1000, + userNewAmount: newCoins, }); console.log(`${commandUserId} change nickname of ${userId}: ${old_nickname} -> ${nickname}`); @@ -640,8 +615,8 @@ export function apiRoutes(client, io) { router.post("/spam-ping", async (req, res) => { const { userId, commandUserId } = req.body; - const user = getUser.get(userId); - const commandUser = getUser.get(commandUserId); + const user = await userService.getUser(userId); + const commandUser = await userService.getUser(commandUserId); if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" }); @@ -654,17 +629,14 @@ export function apiRoutes(client, io) { res.status(200).json({ message: "C'est parti ehehe" }); - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 5000, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - 5000); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "SPAM_PING", - target_user_id: userId, - coins_amount: -5000, - user_new_amount: commandUser.coins - 5000, + targetUserId: userId, + coinsAmount: -5000, + userNewAmount: commandUser.coins - 5000, }); await emitDataUpdated({ table: "users", action: "update" }); @@ -700,8 +672,8 @@ export function apiRoutes(client, io) { router.post("/slowmode", async (req, res) => { let { userId, commandUserId } = req.body; - const user = getUser.get(userId); - const commandUser = getUser.get(commandUserId); + const user = await userService.getUser(userId); + const commandUser = await userService.getUser(commandUserId); if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" }); @@ -714,17 +686,14 @@ export function apiRoutes(client, io) { delete activeSlowmodes[userId]; await socketEmit("new-slowmode", { action: "new slowmode" }); - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 10000, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - 10000); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "SLOWMODE", - target_user_id: userId, - coins_amount: -10000, - user_new_amount: commandUser.coins - 10000, + targetUserId: userId, + coinsAmount: -10000, + userNewAmount: commandUser.coins - 10000, }); try { @@ -759,17 +728,14 @@ export function apiRoutes(client, io) { }; await socketEmit("new-slowmode", { action: "new slowmode" }); - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 10000, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - 10000); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "SLOWMODE", - target_user_id: userId, - coins_amount: -10000, - user_new_amount: commandUser.coins - 10000, + targetUserId: userId, + coinsAmount: -10000, + userNewAmount: commandUser.coins - 10000, }); await emitDataUpdated({ table: "users", action: "update" }); @@ -796,8 +762,8 @@ export function apiRoutes(client, io) { router.post("/timeout", async (req, res) => { let { userId, commandUserId } = req.body; - const user = getUser.get(userId); - const commandUser = getUser.get(commandUserId); + const user = await userService.getUser(userId); + const commandUser = await userService.getUser(commandUserId); if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" }); @@ -830,17 +796,14 @@ export function apiRoutes(client, io) { return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` }); } - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 10000, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - 10000); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "TIMEOUT", - target_user_id: userId, - coins_amount: -10000, - user_new_amount: commandUser.coins - 10000, + targetUserId: userId, + coinsAmount: -10000, + userNewAmount: commandUser.coins - 10000, }); try { @@ -879,17 +842,14 @@ export function apiRoutes(client, io) { await socketEmit("new-timeout", { action: "new timeout" }); - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 100000, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - 100000); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "TIMEOUT", - target_user_id: userId, - coins_amount: -100000, - user_new_amount: commandUser.coins - 100000, + targetUserId: userId, + coinsAmount: -100000, + userNewAmount: commandUser.coins - 100000, }); await emitDataUpdated({ table: "users", action: "update" }); @@ -918,7 +878,7 @@ export function apiRoutes(client, io) { router.post("/start-predi", async (req, res) => { let { commandUserId, label, options, closingTime, payoutTime } = req.body; - const commandUser = getUser.get(commandUserId); + const commandUser = await userService.getUser(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" }); @@ -997,17 +957,14 @@ export function apiRoutes(client, io) { }; await socketEmit("new-predi", { action: "new predi" }); - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - 100, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - 100); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "START_PREDI", - target_user_id: null, - coins_amount: -100, - user_new_amount: commandUser.coins - 100, + targetUserId: null, + coinsAmount: -100, + userNewAmount: commandUser.coins - 100, }); await emitDataUpdated({ table: "users", action: "update" }); @@ -1022,7 +979,7 @@ export function apiRoutes(client, io) { let intAmount = parseInt(amount); if (intAmount < 10 || intAmount > 250000) return res.status(403).send({ message: "Montant invalide" }); - const commandUser = getUser.get(commandUserId); + const commandUser = await userService.getUser(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" }); @@ -1068,17 +1025,14 @@ export function apiRoutes(client, io) { await socketEmit("new-predi", { action: "new vote" }); - updateUserCoins.run({ - id: commandUserId, - coins: commandUser.coins - intAmount, - }); - insertLog.run({ + await userService.updateUserCoins(commandUserId, commandUser.coins - intAmount); + await logService.insertLog({ id: commandUserId + "-" + Date.now(), - user_id: commandUserId, + userId: commandUserId, action: "PREDI_VOTE", - target_user_id: null, - coins_amount: -intAmount, - user_new_amount: commandUser.coins - intAmount, + targetUserId: null, + coinsAmount: -intAmount, + userNewAmount: commandUser.coins - intAmount, }); await emitDataUpdated({ table: "users", action: "update" }); @@ -1088,7 +1042,7 @@ export function apiRoutes(client, io) { router.post("/end-predi", async (req, res) => { const { commandUserId, predi, confirm, winningOption } = req.body; - const commandUser = getUser.get(commandUserId); + const commandUser = await userService.getUser(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" }); @@ -1099,70 +1053,61 @@ export function apiRoutes(client, io) { if (!confirm) { activePredis[predi].cancelledTime = new Date(); - activePredis[predi].options[0].votes.forEach((v) => { - const tempUser = getUser.get(v.id); + for (const v of activePredis[predi].options[0].votes) { + const tempUser = await userService.getUser(v.id); try { - updateUserCoins.run({ - id: v.id, - coins: tempUser.coins + v.amount, - }); - insertLog.run({ + await userService.updateUserCoins(v.id, tempUser.coins + v.amount); + await logService.insertLog({ id: v.id + "-" + Date.now(), - user_id: v.id, + userId: v.id, action: "PREDI_REFUND", - target_user_id: v.id, - coins_amount: v.amount, - user_new_amount: tempUser.coins + v.amount, + targetUserId: v.id, + coinsAmount: v.amount, + userNewAmount: 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); + } + for (const v of activePredis[predi].options[1].votes) { + const tempUser = await userService.getUser(v.id); try { - updateUserCoins.run({ - id: v.id, - coins: tempUser.coins + v.amount, - }); - insertLog.run({ + await userService.updateUserCoins(v.id, tempUser.coins + v.amount); + await logService.insertLog({ id: v.id + "-" + Date.now(), - user_id: v.id, + userId: v.id, action: "PREDI_REFUND", - target_user_id: v.id, - coins_amount: v.amount, - user_new_amount: tempUser.coins + v.amount, + targetUserId: v.id, + coinsAmount: v.amount, + userNewAmount: 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); + for (const v of activePredis[predi].options[winningOption].votes) { + const tempUser = await userService.getUser(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({ + await userService.updateUserCoins(v.id, tempUser.coins + v.amount * (1 + ratio)); + await logService.insertLog({ id: v.id + "-" + Date.now(), - user_id: v.id, + userId: 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), + targetUserId: v.id, + coinsAmount: v.amount * (1 + ratio), + userNewAmount: 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; @@ -1213,18 +1158,18 @@ export function apiRoutes(client, io) { const { discordId, score, isWin } = req.body; console.log(`[SNAKE][SOLO]${discordId}: score=${score}, isWin=${isWin}`); try { - const user = getUser.get(discordId); + const user = await userService.getUser(discordId); if (!user) return res.status(404).json({ message: "Utilisateur introuvable" }); const reward = isWin ? score * 2 : score; const newCoins = user.coins + reward; - updateUserCoins.run({ id: discordId, coins: newCoins }); - insertLog.run({ + await userService.updateUserCoins(discordId, newCoins); + await logService.insertLog({ id: `${discordId}-snake-reward-${Date.now()}`, - user_id: discordId, + userId: discordId, action: "SNAKE_GAME_REWARD", - coins_amount: reward, - user_new_amount: newCoins, - target_user_id: null, + coinsAmount: reward, + userNewAmount: newCoins, + targetUserId: null, }); await emitDataUpdated({ table: "users", action: "update" }); return res.status(200).json({ message: `Récompense de ${reward} FlopoCoins attribuée !` }); @@ -1306,7 +1251,7 @@ export function apiRoutes(client, io) { return res.status(400).json({ error: "Invalid offer" }); } - const user = getUser.get(userId); + const user = await userService.getUser(userId); if (!user) { return res.status(404).json({ error: "User not found" }); } @@ -1394,14 +1339,14 @@ export function apiRoutes(client, io) { } // Check for duplicate processing (idempotency) - const existingTransaction = getTransactionBySessionId.get(session.id); + const existingTransaction = await transactionService.getTransactionBySessionId(session.id); if (existingTransaction) { console.log(`Payment already processed: ${session.id}`); return res.status(200).json({ message: "Already processed" }); } // Get user - const user = getUser.get(commandUserId); + const user = await userService.getUser(commandUserId); if (!user) { console.error(`User not found: ${commandUserId}`); return res.status(404).json({ error: "User not found" }); @@ -1409,30 +1354,30 @@ export function apiRoutes(client, io) { // Update coins const newCoins = user.coins + expectedCoins; - updateUserCoins.run({ id: commandUserId, coins: newCoins }); - + await userService.updateUserCoins(commandUserId, newCoins); + // Insert transaction record const transactionId = `${commandUserId}-transaction-${Date.now()}`; - insertTransaction.run({ + await transactionService.insertTransaction({ id: transactionId, - session_id: session.id, - user_id: commandUserId, - coins_amount: expectedCoins, - amount_cents: amountPaid, + sessionId: session.id, + userId: commandUserId, + coinsAmount: expectedCoins, + amountCents: amountPaid, currency: currency, - customer_email: customerEmail, - customer_name: customerName, - payment_status: session.payment_status, + customerEmail: customerEmail, + customerName: customerName, + paymentStatus: session.payment_status, }); - + // Insert log entry - insertLog.run({ + await logService.insertLog({ id: `${commandUserId}-buycoins-${Date.now()}`, - user_id: commandUserId, + userId: commandUserId, action: "BUY_COINS", - target_user_id: null, - coins_amount: expectedCoins, - user_new_amount: newCoins, + targetUserId: null, + coinsAmount: expectedCoins, + userNewAmount: newCoins, }); console.log(`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid/100} ${currency}`); diff --git a/src/server/routes/blackjack.js b/src/server/routes/blackjack.js index 4b9f5c6..b9703ba 100644 --- a/src/server/routes/blackjack.js +++ b/src/server/routes/blackjack.js @@ -15,7 +15,8 @@ import { } from "../../game/blackjack.js"; // Optional: hook into your DB & Discord systems if available -import { getUser, insertLog, updateUserCoins } from "../../database/index.js"; +import * as userService from "../../services/user.service.js"; +import * as logService from "../../services/log.service.js"; import { client } from "../../bot/client.js"; import { emitToast, emitUpdate, emitPlayerUpdate } from "../socket.js"; import { EmbedBuilder, time } from "discord.js"; @@ -128,7 +129,7 @@ export function blackjackRoutes(io) { 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 bank = (await userService.getUser(userId))?.coins ?? 0; room.players[userId] = { id: userId, @@ -229,7 +230,7 @@ export function blackjackRoutes(io) { } }); - router.post("/bet", (req, res) => { + router.post("/bet", async (req, res) => { const { userId, amount } = req.body; const p = room.players[userId]; if (!p) return res.status(404).json({ message: "not in room" }); @@ -239,17 +240,17 @@ export function blackjackRoutes(io) { 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 userDB = await userService.getUser(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({ + await userService.updateUserCoins(userId, coins - bet); + await logService.insertLog({ id: `${userId}-blackjack-${Date.now()}`, - user_id: userId, - target_user_id: null, + userId: userId, + targetUserId: null, action: "BLACKJACK_BET", - coins_amount: -bet, - user_new_amount: coins - bet, + coinsAmount: -bet, + userNewAmount: coins - bet, }); p.bank = coins - bet; } @@ -261,7 +262,7 @@ export function blackjackRoutes(io) { return res.status(200).json({ message: "bet-accepted" }); }); - router.post("/action/:action", (req, res) => { + router.post("/action/:action", async (req, res) => { const { userId } = req.body; const action = req.params.action; const p = room.players[userId]; @@ -270,36 +271,36 @@ export function blackjackRoutes(io) { // Handle extra coin lock for double if (action === "double" && !room.settings.fakeMoney) { - const userDB = getUser.get(userId); + const userDB = await userService.getUser(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({ + await userService.updateUserCoins(userId, coins - hand.bet); + await logService.insertLog({ id: `${userId}-blackjack-${Date.now()}`, - user_id: userId, - target_user_id: null, + userId: userId, + targetUserId: null, action: "BLACKJACK_DOUBLE", - coins_amount: -hand.bet, - user_new_amount: coins - hand.bet, + coinsAmount: -hand.bet, + userNewAmount: 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 userDB = await userService.getUser(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({ + await userService.updateUserCoins(userId, coins - hand.bet); + await logService.insertLog({ id: `${userId}-blackjack-${Date.now()}`, - user_id: userId, - target_user_id: null, + userId: userId, + targetUserId: null, action: "BLACKJACK_SPLIT", - coins_amount: -hand.bet, - user_new_amount: coins - hand.bet, + coinsAmount: -hand.bet, + userNewAmount: coins - hand.bet, }); p.bank = coins - hand.bet; // effective bet size is handled in settlement via hand.doubled flag diff --git a/src/server/routes/market.js b/src/server/routes/market.js index 7326448..6303630 100644 --- a/src/server/routes/market.js +++ b/src/server/routes/market.js @@ -5,18 +5,10 @@ import express from "express"; // --- Utility and API Imports --- // --- Discord.js Builder Imports --- import { ButtonStyle } from "discord.js"; -import { - getMarketOfferById, - getMarketOffers, - getMarketOffersBySkin, - getOfferBids, - getSkin, - getUser, - insertBid, - insertLog, - insertMarketOffer, - updateUserCoins, -} from "../../database/index.js"; +import * as userService from "../../services/user.service.js"; +import * as skinService from "../../services/skin.service.js"; +import * as logService from "../../services/log.service.js"; +import * as marketService from "../../services/market.service.js"; import { emitMarketUpdate } from "../socket.js"; import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js"; @@ -32,25 +24,26 @@ const router = express.Router(); export function marketRoutes(client, io) { router.get("/offers", async (req, res) => { try { - const offers = getMarketOffers.all(); - offers.forEach((offer) => { - offer.skin = getSkin.get(offer.skin_uuid); - offer.seller = getUser.get(offer.seller_id); - offer.buyer = getUser.get(offer.buyer_id) || null; - offer.bids = getOfferBids.all(offer.id) || {}; - offer.bids.forEach((bid) => { - bid.bidder = getUser.get(bid.bidder_id); - }); - }); + const offers = await marketService.getMarketOffers(); + for (const offer of offers) { + offer.skin = await skinService.getSkin(offer.skinUuid); + offer.seller = await userService.getUser(offer.sellerId); + offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null; + offer.bids = (await marketService.getOfferBids(offer.id)) || {}; + for (const bid of offer.bids) { + bid.bidder = await userService.getUser(bid.bidderId); + } + } res.status(200).send({ offers }); } catch (e) { + console.log(e); res.status(500).send({ error: e }); } }); router.get("/offers/:id", async (req, res) => { try { - const offer = getMarketOfferById.get(req.params.id); + const offer = await marketService.getMarketOfferById(req.params.id); if (offer) { res.status(200).send({ offer }); } else { @@ -63,7 +56,7 @@ export function marketRoutes(client, io) { router.get("/offers/:id/bids", async (req, res) => { try { - const bids = getOfferBids.get(req.params.id); + const bids = await marketService.getOfferBids(req.params.id); res.status(200).send({ bids }); } catch (e) { res.status(500).send({ error: e }); @@ -74,13 +67,13 @@ export function marketRoutes(client, io) { const { seller_id, skin_uuid, starting_price, delay, duration, timestamp } = req.body; const now = Date.now(); try { - const skin = getSkin.get(skin_uuid); + const skin = await skinService.getSkin(skin_uuid); if (!skin) return res.status(404).send({ error: "Skin not found" }); - const seller = getUser.get(seller_id); + const seller = await userService.getUser(seller_id); if (!seller) return res.status(404).send({ error: "Seller not found" }); - if (skin.user_id !== seller.id) return res.status(403).send({ error: "You do not own this skin" }); + if (skin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" }); - const existingOffers = getMarketOffersBySkin.all(skin.uuid); + const existingOffers = await marketService.getMarketOffersBySkin(skin.uuid); if ( existingOffers.length > 0 && existingOffers.some((offer) => offer.status === "open" || offer.status === "pending") @@ -92,15 +85,15 @@ export function marketRoutes(client, io) { const closing_at = opening_at + duration; const offerId = Date.now() + "-" + seller.id + "-" + skin.uuid; - insertMarketOffer.run({ + await marketService.insertMarketOffer({ id: offerId, - skin_uuid: skin.uuid, - seller_id: seller.id, - starting_price: starting_price, - buyout_price: null, + skinUuid: skin.uuid, + sellerId: seller.id, + startingPrice: starting_price, + buyoutPrice: null, status: delay > 0 ? "pending" : "open", - opening_at: opening_at, - closing_at: closing_at, + openingAt: opening_at, + closingAt: closing_at, }); await emitMarketUpdate(); await handleNewMarketOffer(offerId, client); @@ -114,62 +107,62 @@ export function marketRoutes(client, io) { router.post("/offers/:id/place-bid", async (req, res) => { const { buyer_id, bid_amount, timestamp } = req.body; try { - const offer = getMarketOfferById.get(req.params.id); + const offer = await marketService.getMarketOfferById(req.params.id); if (!offer) return res.status(404).send({ error: "Offer not found" }); - if (offer.closing_at < timestamp) return res.status(403).send({ error: "Bidding period has ended" }); + if (offer.closingAt < timestamp) return res.status(403).send({ error: "Bidding period has ended" }); - if (buyer_id === offer.seller_id) return res.status(403).send({ error: "You can't bid on your own offer" }); + if (buyer_id === offer.sellerId) return res.status(403).send({ error: "You can't bid on your own offer" }); - const offerBids = getOfferBids.all(offer.id); + const offerBids = await marketService.getOfferBids(offer.id); const lastBid = offerBids[0]; if (lastBid) { - if (lastBid?.bidder_id === buyer_id) + if (lastBid?.bidderId === buyer_id) return res.status(403).send({ error: "You are already the highest bidder" }); - if (bid_amount < lastBid?.offer_amount + 10) { + if (bid_amount < lastBid?.offerAmount + 10) { return res.status(403).send({ error: "Bid amount is below minimum" }); } } else { - if (bid_amount < offer.starting_price + 10) { + if (bid_amount < offer.startingPrice + 10) { return res.status(403).send({ error: "Bid amount is below minimum" }); } } - const bidder = getUser.get(buyer_id); + const bidder = await userService.getUser(buyer_id); if (!bidder) return res.status(404).send({ error: "Bidder not found" }); if (bidder.coins < bid_amount) return res.status(403).send({ error: "You do not have enough coins to place this bid" }); const bidId = Date.now() + "-" + buyer_id + "-" + offer.id; - insertBid.run({ + await marketService.insertBid({ id: bidId, - bidder_id: buyer_id, - market_offer_id: offer.id, - offer_amount: bid_amount, + bidderId: buyer_id, + marketOfferId: offer.id, + offerAmount: bid_amount, }); const newCoinsAmount = bidder.coins - bid_amount; - updateUserCoins.run({ id: buyer_id, coins: newCoinsAmount }); - insertLog.run({ + await userService.updateUserCoins(buyer_id, newCoinsAmount); + await logService.insertLog({ id: `${buyer_id}-bid-${offer.id}-${Date.now()}`, - user_id: buyer_id, + userId: buyer_id, action: "BID_PLACED", - target_user_id: null, - coins_amount: bid_amount, - user_new_amount: newCoinsAmount, + targetUserId: null, + coinsAmount: bid_amount, + userNewAmount: newCoinsAmount, }); // Refund the previous highest bidder if (lastBid) { - const previousBidder = getUser.get(lastBid.bidder_id); - const refundedCoinsAmount = previousBidder.coins + lastBid.offer_amount; - updateUserCoins.run({ id: previousBidder.id, coins: refundedCoinsAmount }); - insertLog.run({ + const previousBidder = await userService.getUser(lastBid.bidderId); + const refundedCoinsAmount = previousBidder.coins + lastBid.offerAmount; + await userService.updateUserCoins(previousBidder.id, refundedCoinsAmount); + await logService.insertLog({ id: `${previousBidder.id}-bid-refund-${offer.id}-${Date.now()}`, - user_id: previousBidder.id, + userId: previousBidder.id, action: "BID_REFUNDED", - target_user_id: null, - coins_amount: lastBid.offer_amount, - user_new_amount: refundedCoinsAmount, + targetUserId: null, + coinsAmount: lastBid.offerAmount, + userNewAmount: refundedCoinsAmount, }); } diff --git a/src/server/routes/monke.js b/src/server/routes/monke.js index 52a0599..e208b7a 100644 --- a/src/server/routes/monke.js +++ b/src/server/routes/monke.js @@ -2,7 +2,8 @@ import express from "express"; import { v4 as uuidv4 } from "uuid"; import { monkePaths } from "../../game/state.js"; import { socketEmit } from "../socket.js"; -import { getUser, updateUserCoins, insertLog } from "../../database/index.js"; +import * as userService from "../../services/user.service.js"; +import * as logService from "../../services/log.service.js"; import { init } from "openai/_shims/index.mjs"; const router = express.Router(); @@ -16,11 +17,11 @@ const router = express.Router(); export function monkeRoutes(client, io) { // --- Router Management Endpoints - router.get("/:userId", (req, res) => { + router.get("/:userId", async (req, res) => { const { userId } = req.params; if (!userId) return res.status(400).json({ error: "User ID is required" }); - const user = getUser.get(userId); + const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); const userGamePath = monkePaths[userId] || null; if (!userGamePath) return res.status(404).json({ error: "No active game found for this user" }); @@ -28,29 +29,26 @@ export function monkeRoutes(client, io) { return res.status(200).json({ userGamePath }); }); - router.post("/:userId/start", (req, res) => { + router.post("/:userId/start", async (req, res) => { const { userId } = req.params; const { initialBet } = req.body; if (!userId) return res.status(400).json({ error: "User ID is required" }); - const user = getUser.get(userId); + const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!initialBet) return res.status(400).json({ error: "Initial bet is required" }); if (initialBet > user.coins) return res.status(400).json({ error: "Insufficient coins for the initial bet" }); try { const newCoins = user.coins - initialBet; - updateUserCoins.run({ - id: userId, - coins: newCoins, - }); - insertLog.run({ + await userService.updateUserCoins(userId, newCoins); + await logService.insertLog({ id: `${userId}-monke-bet-${Date.now()}`, - user_id: userId, - target_user_id: null, + userId: userId, + targetUserId: null, action: "MONKE_BET", - coins_amount: -initialBet, - user_new_amount: newCoins, + coinsAmount: -initialBet, + userNewAmount: newCoins, }); } catch (error) { return res.status(500).json({ error: "Failed to update user coins" }); @@ -61,12 +59,12 @@ export function monkeRoutes(client, io) { return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] }); }); - router.post("/:userId/play", (req, res) => { + router.post("/:userId/play", async (req, res) => { const { userId } = req.params; const { choice, step } = req.body; if (!userId) return res.status(400).json({ error: "User ID is required" }); - const user = getUser.get(userId); + const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" }); @@ -97,10 +95,10 @@ export function monkeRoutes(client, io) { } }); - router.post("/:userId/stop", (req, res) => { + router.post("/:userId/stop", async (req, res) => { const { userId } = req.params; if (!userId) return res.status(400).json({ error: "User ID is required" }); - const user = getUser.get(userId); + const user = await userService.getUser(userId); if (!user) return res.status(404).json({ error: "User not found" }); if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" }); const userGamePath = monkePaths[userId]; @@ -112,17 +110,14 @@ export function monkeRoutes(client, io) { const newCoins = coins + extractValue; try { - updateUserCoins.run({ - id: userId, - coins: newCoins, - }); - insertLog.run({ + await userService.updateUserCoins(userId, newCoins); + await logService.insertLog({ id: `${userId}-monke-withdraw-${Date.now()}`, - user_id: userId, - target_user_id: null, + userId: userId, + targetUserId: null, action: "MONKE_WITHDRAW", - coins_amount: extractValue, - user_new_amount: newCoins, + coinsAmount: extractValue, + userNewAmount: newCoins, }); return res.status(200).json({ message: "Game stopped", userGamePath }); diff --git a/src/server/routes/poker.js b/src/server/routes/poker.js index 70b0a31..9439282 100644 --- a/src/server/routes/poker.js +++ b/src/server/routes/poker.js @@ -10,7 +10,8 @@ import { getNextActivePlayer, initialShuffledCards, } from "../../game/poker.js"; -import { getUser, insertLog, updateUserCoins } from "../../database/index.js"; +import * as userService from "../../services/user.service.js"; +import * as logService from "../../services/log.service.js"; import { sleep } from "openai/core"; import { client } from "../../bot/client.js"; import { emitPokerToast, emitPokerUpdate } from "../socket.js"; @@ -131,7 +132,7 @@ export function pokerRoutes(client, io) { 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)) { + if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > ((await userService.getUser(userId))?.coins ?? 0)) { return res.status(403).json({ message: "You do not have enough coins to join this room." }); } @@ -147,19 +148,16 @@ export function pokerRoutes(client, io) { } if (!room.fakeMoney) { - const userDB = getUser.get(playerId); + const userDB = await userService.getUser(playerId); if (userDB) { - updateUserCoins.run({ - id: playerId, - coins: userDB.coins - room.minBet, - }); - insertLog.run({ + await userService.updateUserCoins(playerId, userDB.coins - room.minBet); + await logService.insertLog({ id: `${playerId}-poker-${Date.now()}`, - user_id: playerId, - target_user_id: null, + userId: playerId, + targetUserId: null, action: "POKER_JOIN", - coins_amount: -room.minBet, - user_new_amount: userDB.coins - room.minBet, + coinsAmount: -room.minBet, + userNewAmount: userDB.coins - room.minBet, }); } } @@ -199,7 +197,7 @@ export function pokerRoutes(client, io) { } try { - updatePlayerCoins( + await updatePlayerCoins( pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney, @@ -239,7 +237,7 @@ export function pokerRoutes(client, io) { } try { - updatePlayerCoins( + await updatePlayerCoins( pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].fakeMoney, @@ -359,7 +357,7 @@ export function pokerRoutes(client, io) { async function joinRoom(roomId, userId, io) { const user = await client.users.fetch(userId); - const userDB = getUser.get(userId); + const userDB = await userService.getUser(userId); const room = pokerRooms[roomId]; const playerObject = { @@ -380,14 +378,14 @@ async function joinRoom(roomId, userId, io) { } else { room.players[userId] = playerObject; if (!room.fakeMoney) { - updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet }); - insertLog.run({ + await userService.updateUserCoins(userId, userDB.coins - room.minBet); + await logService.insertLog({ id: `${userId}-poker-${Date.now()}`, - user_id: userId, - target_user_id: null, + userId: userId, + targetUserId: null, action: "POKER_JOIN", - coins_amount: -room.minBet, - user_new_amount: userDB.coins - room.minBet, + coinsAmount: -room.minBet, + userNewAmount: userDB.coins - room.minBet, }); } } @@ -539,29 +537,29 @@ function updatePlayerHandSolves(room) { } } -function updatePlayerCoins(player, amount, isFake) { +async function updatePlayerCoins(player, amount, isFake) { if (isFake) return; - const user = getUser.get(player.id); + const user = await userService.getUser(player.id); if (!user) return; - const userDB = getUser.get(player.id); - updateUserCoins.run({ id: player.id, coins: userDB.coins + amount }); - insertLog.run({ + const userDB = await userService.getUser(player.id); + await userService.updateUserCoins(player.id, userDB.coins + amount); + await logService.insertLog({ id: `${player.id}-poker-${Date.now()}`, - user_id: player.id, - target_user_id: null, + userId: player.id, + targetUserId: null, action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`, - coins_amount: amount, - user_new_amount: userDB.coins + amount, + coinsAmount: amount, + userNewAmount: userDB.coins + amount, }); } async function clearAfkPlayers(room) { - Object.keys(room.afk).forEach((playerId) => { + for (const playerId of Object.keys(room.afk)) { if (room.players[playerId]) { - updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney); + await 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 3d5abb5..affcd0a 100644 --- a/src/server/routes/solitaire.js +++ b/src/server/routes/solitaire.js @@ -19,16 +19,9 @@ import { // --- Game State & Database Imports --- import { activeSolitaireGames } from "../../game/state.js"; -import { - getSOTD, - getUser, - insertSOTDStats, - deleteUserSOTDStats, - getUserSOTDStats, - updateUserCoins, - insertLog, - getAllSOTDStats, -} from "../../database/index.js"; +import * as userService from "../../services/user.service.js"; +import * as logService from "../../services/log.service.js"; +import * as solitaireService from "../../services/solitaire.service.js"; import { socketEmit } from "../socket.js"; // Create a new router instance @@ -85,7 +78,7 @@ export function solitaireRoutes(client, io) { res.json({ success: true, gameState }); }); - router.post("/start/sotd", (req, res) => { + router.post("/start/sotd", async (req, res) => { const { userId } = req.body; /*if (!userId || !getUser.get(userId)) { return res.status(404).json({ error: 'User not found.' }); @@ -98,7 +91,7 @@ export function solitaireRoutes(client, io) { }); } - const sotd = getSOTD.get(); + const sotd = await solitaireService.getSOTD(); if (!sotd) { return res.status(500).json({ error: "Solitaire of the Day is not configured." }); } @@ -126,9 +119,9 @@ export function solitaireRoutes(client, io) { // --- Game State & Action Endpoints --- - router.get("/sotd/rankings", (req, res) => { + router.get("/sotd/rankings", async (req, res) => { try { - const rankings = getAllSOTDStats.all(); + const rankings = await solitaireService.getAllSOTDStats(); res.json({ rankings }); } catch (e) { res.status(500).json({ error: "Failed to fetch SOTD rankings." }); @@ -237,20 +230,20 @@ function updateGameStats(gameState, actionType, moveData = {}) { /** Handles the logic when a game is won. */ async function handleWin(userId, gameState, io) { - const currentUser = getUser.get(userId); + const currentUser = await userService.getUser(userId); if (!currentUser) return; if (gameState.hardMode) { const bonus = 100; const newCoins = currentUser.coins + bonus; - updateUserCoins.run({ id: userId, coins: newCoins }); - insertLog.run({ + await userService.updateUserCoins(userId, newCoins); + await logService.insertLog({ id: `${userId}-hardmode-solitaire-${Date.now()}`, - user_id: userId, + userId: userId, action: "HARDMODE_SOLITAIRE_WIN", - target_user_id: null, - coins_amount: bonus, - user_new_amount: newCoins, + targetUserId: null, + coinsAmount: bonus, + userNewAmount: newCoins, }); await socketEmit("data-updated", { table: "users" }); } @@ -260,20 +253,20 @@ async function handleWin(userId, gameState, io) { gameState.endTime = Date.now(); const timeTaken = gameState.endTime - gameState.startTime; - const existingStats = getUserSOTDStats.get(userId); + const existingStats = await solitaireService.getUserSOTDStats(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({ + await userService.updateUserCoins(userId, newCoins); + await logService.insertLog({ id: `${userId}-sotd-complete-${Date.now()}`, - user_id: userId, + userId: userId, action: "SOTD_WIN", - target_user_id: null, - coins_amount: bonus, - user_new_amount: newCoins, + targetUserId: null, + coinsAmount: bonus, + userNewAmount: newCoins, }); await socketEmit("data-updated", { table: "users" }); } @@ -288,10 +281,10 @@ async function handleWin(userId, gameState, io) { timeTaken < existingStats.time); if (isNewBest) { - deleteUserSOTDStats.run(userId); - insertSOTDStats.run({ + await solitaireService.deleteUserSOTDStats(userId); + await solitaireService.insertSOTDStats({ id: userId, - user_id: userId, + userId: userId, time: timeTaken, moves: gameState.moves, score: gameState.score, diff --git a/src/services/game.service.js b/src/services/game.service.js new file mode 100644 index 0000000..77fd29e --- /dev/null +++ b/src/services/game.service.js @@ -0,0 +1,49 @@ +import prisma from "../prisma/client.js"; + +export async function getUserElo(id) { + return prisma.elo.findUnique({ where: { id } }); +} + +export async function insertElo(id, elo) { + return prisma.elo.create({ data: { id, elo } }); +} + +export async function updateElo(id, elo) { + return prisma.elo.update({ where: { id }, data: { elo } }); +} + +export async function getUsersByElo() { + const users = await prisma.user.findMany({ + include: { elo: true }, + orderBy: { elo: { elo: "desc" } }, + }); + return users + .filter((u) => u.elo) + .map((u) => ({ ...u, elo: u.elo?.elo ?? null })); +} + +function toGame(game) { + return { ...game, timestamp: game.timestamp != null ? game.timestamp.getTime() : null }; +} + +export async function insertGame(data) { + return prisma.game.create({ + data: { + ...data, + timestamp: data.timestamp != null ? new Date(data.timestamp) : null, + }, + }); +} + +export async function getGames() { + const games = await prisma.game.findMany(); + return games.map(toGame); +} + +export async function getUserGames(userId) { + const games = await prisma.game.findMany({ + where: { OR: [{ p1: userId }, { p2: userId }] }, + orderBy: { timestamp: "asc" }, + }); + return games.map(toGame); +} diff --git a/src/services/log.service.js b/src/services/log.service.js new file mode 100644 index 0000000..b2f1fd3 --- /dev/null +++ b/src/services/log.service.js @@ -0,0 +1,33 @@ +import prisma from "../prisma/client.js"; + +export async function insertLog(data) { + return prisma.log.create({ data }); +} + +export async function getLogs() { + return prisma.log.findMany(); +} + +export async function getUserLogs(userId) { + return prisma.log.findMany({ where: { userId } }); +} + +export async function pruneOldLogs() { + const limit = parseInt(process.env.LOGS_BY_USER); + const usersWithExcess = await prisma.$queryRawUnsafe( + `SELECT user_id as userId FROM logs GROUP BY user_id HAVING COUNT(*) > ?`, + limit, + ); + for (const { userId } of usersWithExcess) { + await prisma.$executeRawUnsafe( + `DELETE FROM logs WHERE id IN ( + SELECT id FROM ( + SELECT id, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn + FROM logs WHERE user_id = ? + ) WHERE rn > ? + )`, + userId, + limit, + ); + } +} diff --git a/src/services/market.service.js b/src/services/market.service.js new file mode 100644 index 0000000..fe3aa5a --- /dev/null +++ b/src/services/market.service.js @@ -0,0 +1,107 @@ +import prisma from "../prisma/client.js"; + +function toOffer(offer) { + return { ...offer, openingAt: offer.openingAt.getTime(), closingAt: offer.closingAt.getTime() }; +} + +export async function getMarketOffers() { + const offers = await prisma.marketOffer.findMany({ orderBy: { postedAt: "desc" } }); + return offers.map(toOffer); +} + +export async function getMarketOfferById(id) { + const offer = await prisma.marketOffer.findUnique({ + where: { id }, + include: { + skin: { select: { displayName: true, displayIcon: true } }, + seller: { select: { username: true, globalName: true } }, + buyer: { select: { username: true, globalName: true } }, + }, + }); + if (!offer) return null; + // Flatten to match the old query shape + return toOffer({ + ...offer, + skinName: offer.skin?.displayName, + skinIcon: offer.skin?.displayIcon, + sellerName: offer.seller?.username, + sellerGlobalName: offer.seller?.globalName, + buyerName: offer.buyer?.username ?? null, + buyerGlobalName: offer.buyer?.globalName ?? null, + }); +} + +export async function getMarketOffersBySkin(skinUuid) { + const offers = await prisma.marketOffer.findMany({ + where: { skinUuid }, + include: { + skin: { select: { displayName: true, displayIcon: true } }, + seller: { select: { username: true, globalName: true } }, + buyer: { select: { username: true, globalName: true } }, + }, + }); + return offers.map((offer) => toOffer({ + ...offer, + skinName: offer.skin?.displayName, + skinIcon: offer.skin?.displayIcon, + sellerName: offer.seller?.username, + sellerGlobalName: offer.seller?.globalName, + buyerName: offer.buyer?.username ?? null, + buyerGlobalName: offer.buyer?.globalName ?? null, + })); +} + +export async function insertMarketOffer(data) { + return prisma.marketOffer.create({ + data: { + ...data, + openingAt: new Date(data.openingAt), + closingAt: new Date(data.closingAt), + }, + }); +} + +export async function updateMarketOffer(data) { + const { id, ...rest } = data; + return prisma.marketOffer.update({ where: { id }, data: rest }); +} + +export async function deleteMarketOffer(id) { + return prisma.marketOffer.delete({ where: { id } }); +} + +// --- Bids --- + +export async function getBids() { + const bids = await prisma.bid.findMany({ + include: { bidder: { select: { username: true, globalName: true } } }, + orderBy: [{ offerAmount: "desc" }, { offeredAt: "asc" }], + }); + return bids.map((bid) => ({ + ...bid, + bidderName: bid.bidder?.username, + bidderGlobalName: bid.bidder?.globalName, + })); +} + +export async function getBidById(id) { + return prisma.bid.findUnique({ where: { id } }); +} + +export async function getOfferBids(marketOfferId) { + const bids = await prisma.bid.findMany({ + where: { marketOfferId }, + orderBy: [{ offerAmount: "desc" }, { offeredAt: "asc" }], + }); + return bids.map((bid) => ({ + ...bid, + })); +} + +export async function insertBid(data) { + return prisma.bid.create({ data }); +} + +export async function deleteBid(id) { + return prisma.bid.delete({ where: { id } }); +} diff --git a/src/services/skin.service.js b/src/services/skin.service.js new file mode 100644 index 0000000..615ad5f --- /dev/null +++ b/src/services/skin.service.js @@ -0,0 +1,59 @@ +import prisma from "../prisma/client.js"; + +export async function getSkin(uuid) { + return prisma.skin.findUnique({ where: { uuid } }); +} + +export async function getAllSkins() { + return prisma.skin.findMany({ orderBy: { maxPrice: "desc" } }); +} + +export async function getAllAvailableSkins() { + return prisma.skin.findMany({ where: { userId: null } }); +} + +export async function getUserInventory(userId) { + return prisma.skin.findMany({ + where: { userId }, + orderBy: { currentPrice: "desc" }, + }); +} + +export async function getTopSkins() { + return prisma.skin.findMany({ orderBy: { maxPrice: "desc" }, take: 10 }); +} + +export async function insertSkin(data) { + return prisma.skin.create({ data }); +} + +export async function updateSkin(data) { + const { uuid, ...rest } = data; + return prisma.skin.update({ where: { uuid }, data: rest }); +} + +export async function hardUpdateSkin(data) { + const { uuid, ...rest } = data; + return prisma.skin.update({ where: { uuid }, data: rest }); +} + +export async function insertManySkins(skins) { + return prisma.$transaction( + skins.map((skin) => + prisma.skin.upsert({ + where: { uuid: skin.uuid }, + update: {}, + create: skin, + }), + ), + ); +} + +export async function updateManySkins(skins) { + return prisma.$transaction( + skins.map((skin) => { + const { uuid, ...data } = skin; + return prisma.skin.update({ where: { uuid }, data }); + }), + ); +} diff --git a/src/services/solitaire.service.js b/src/services/solitaire.service.js new file mode 100644 index 0000000..619d7db --- /dev/null +++ b/src/services/solitaire.service.js @@ -0,0 +1,40 @@ +import prisma from "../prisma/client.js"; + +export async function getSOTD() { + return prisma.sotd.findUnique({ where: { id: 0 } }); +} + +export async function insertSOTD(data) { + return prisma.sotd.create({ data: { id: 0, ...data } }); +} + +export async function deleteSOTD() { + return prisma.sotd.delete({ where: { id: 0 } }).catch(() => {}); +} + +export async function getAllSOTDStats() { + const stats = await prisma.sotdStat.findMany({ + include: { user: { select: { globalName: true } } }, + orderBy: [{ score: "desc" }, { moves: "asc" }, { time: "asc" }], + }); + return stats.map((s) => ({ + ...s, + globalName: s.user?.globalName, + })); +} + +export async function getUserSOTDStats(userId) { + return prisma.sotdStat.findFirst({ where: { userId } }); +} + +export async function insertSOTDStats(data) { + return prisma.sotdStat.create({ data }); +} + +export async function clearSOTDStats() { + return prisma.sotdStat.deleteMany(); +} + +export async function deleteUserSOTDStats(userId) { + return prisma.sotdStat.deleteMany({ where: { userId } }); +} diff --git a/src/services/transaction.service.js b/src/services/transaction.service.js new file mode 100644 index 0000000..5aa9a0c --- /dev/null +++ b/src/services/transaction.service.js @@ -0,0 +1,20 @@ +import prisma from "../prisma/client.js"; + +export async function insertTransaction(data) { + return prisma.transaction.create({ data }); +} + +export async function getTransactionBySessionId(sessionId) { + return prisma.transaction.findUnique({ where: { sessionId } }); +} + +export async function getAllTransactions() { + return prisma.transaction.findMany({ orderBy: { createdAt: "desc" } }); +} + +export async function getUserTransactions(userId) { + return prisma.transaction.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + }); +} diff --git a/src/services/user.service.js b/src/services/user.service.js new file mode 100644 index 0000000..37ba33a --- /dev/null +++ b/src/services/user.service.js @@ -0,0 +1,73 @@ +import prisma from "../prisma/client.js"; + +export async function getUser(id) { + const user = await prisma.user.findUnique({ + where: { id }, + include: { elo: true }, + }); + if (!user) return null; + return { ...user, elo: user.elo?.elo ?? null }; +} + +export async function getAllUsers() { + const users = await prisma.user.findMany({ + include: { elo: true }, + orderBy: { coins: "desc" }, + }); + return users.map((u) => ({ ...u, elo: u.elo?.elo ?? null })); +} + +export async function getAllAkhys() { + const users = await prisma.user.findMany({ + where: { isAkhy: 1 }, + include: { elo: true }, + orderBy: { coins: "desc" }, + }); + return users.map((u) => ({ ...u, elo: u.elo?.elo ?? null })); +} + +export async function insertUser(data) { + return prisma.user.create({ data }); +} + +export async function updateUser(data) { + const { id, ...rest } = data; + return prisma.user.update({ where: { id }, data: rest }); +} + +export async function updateUserCoins(id, coins) { + return prisma.user.update({ where: { id }, data: { coins } }); +} + +export async function updateUserAvatar(id, avatarUrl) { + return prisma.user.update({ where: { id }, data: { avatarUrl } }); +} + +export async function queryDailyReward(id) { + return prisma.user.update({ where: { id }, data: { dailyQueried: 1 } }); +} + +export async function resetDailyReward() { + return prisma.user.updateMany({ data: { dailyQueried: 0 } }); +} + +export async function insertManyUsers(users) { + return prisma.$transaction( + users.map((user) => + prisma.user.upsert({ + where: { id: user.id }, + update: {}, + create: user, + }), + ), + ); +} + +export async function updateManyUsers(users) { + return prisma.$transaction( + users.map((user) => { + const { id, elo, ...data } = user; + return prisma.user.update({ where: { id }, data }); + }), + ); +} diff --git a/src/utils/caseOpening.js b/src/utils/caseOpening.js index 90c1463..17b08c7 100644 --- a/src/utils/caseOpening.js +++ b/src/utils/caseOpening.js @@ -1,4 +1,4 @@ -import { getAllAvailableSkins, getSkin } from "../database/index.js"; +import * as skinService from "../services/skin.service.js"; import { skins } from "../game/state.js"; import { isChampionsSkin } from "./index.js"; @@ -6,16 +6,15 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) { if (caseType === "esport") { // Esport case: return all esport skins try { - const dbSkins = getAllAvailableSkins.all(); - const esportSkins = skins - .filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid)) - .map((s) => { - const dbSkin = getSkin.get(s.uuid); - return { - ...s, // Shallow copy to avoid mutating the imported 'skins' object - tierColor: dbSkin?.tierColor, - }; + const dbSkins = await skinService.getAllAvailableSkins(); + const esportSkins = []; + for (const s of skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid))) { + const dbSkin = await skinService.getSkin(s.uuid); + esportSkins.push({ + ...s, + tierColor: dbSkin?.tierColor, }); + } return esportSkins; } catch (e) { console.log(e); @@ -55,8 +54,8 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) { } try { - const dbSkins = getAllAvailableSkins.all(); - const weightedPool = skins + const dbSkins = await skinService.getAllAvailableSkins(); + const filtered = skins .filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid)) .filter((s) => { if (caseType === "ultra") { @@ -71,16 +70,19 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) { } else { return isChampionsSkin(s.displayName) === false; } - }) - .map((s) => { - const dbSkin = getSkin.get(s.uuid); - return { - ...s, // Shallow copy to avoid mutating the imported 'skins' object + }); + const weightedPool = []; + for (const s of filtered) { + const dbSkin = await skinService.getSkin(s.uuid); + const weight = tierWeights[s.contentTierUuid] ?? 0; + if (weight > 0) { // <--- CRITICAL: Remove 0 weight skins + weightedPool.push({ + ...s, tierColor: dbSkin?.tierColor, - weight: tierWeights[s.contentTierUuid] ?? 0, - }; - }) - .filter((s) => s.weight > 0); // <--- CRITICAL: Remove 0 weight skins + weight, + }); + } + } function weightedSample(arr, count) { let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0); @@ -123,7 +125,7 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) { } } -export function drawCaseSkin(caseContent) { +export async function drawCaseSkin(caseContent) { let randomSelectedSkinIndex; let randomSelectedSkinUuid; try { @@ -134,7 +136,7 @@ export function drawCaseSkin(caseContent) { throw new Error("Failed to draw a skin from the case content."); } - const dbSkin = getSkin.get(randomSelectedSkinUuid); + const dbSkin = await skinService.getSkin(randomSelectedSkinUuid); const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); if (!randomSkinData) { throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); diff --git a/src/utils/index.js b/src/utils/index.js index 0921084..f6bf51d 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -5,23 +5,9 @@ import cron from "node-cron"; import { getSkinTiers, getValorantSkins } from "../api/valorant.js"; import { DiscordRequest } from "../api/discord.js"; import { initTodaysSOTD } from "../game/points.js"; -import { - deleteBid, - deleteMarketOffer, - getAllAkhys, - getAllUsers, - getMarketOffers, - getOfferBids, - getSkin, - getUser, - insertManySkins, - insertUser, - resetDailyReward, - updateMarketOffer, - updateSkin, - updateUserAvatar, - updateUserCoins, -} from "../database/index.js"; +import * as userService from "../services/user.service.js"; +import * as skinService from "../services/skin.service.js"; +import * as marketService from "../services/market.service.js"; import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js"; import { emitMarketUpdate } from "../server/socket.js"; import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js"; @@ -49,7 +35,7 @@ export async function InstallGlobalCommands(appId, commands) { export async function getAkhys(client) { try { // 1. Fetch Discord Members - const initial_akhys = getAllUsers.all().length; + const initial_akhys = (await userService.getAllUsers()).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)); @@ -67,14 +53,14 @@ export async function getAkhys(client) { })); if (usersToInsert.length > 0) { - usersToInsert.forEach((user) => { + for (const user of usersToInsert) { try { - insertUser.run(user); + await userService.insertUser(user); } catch (err) {} - }); + } } - const new_akhys = getAllUsers.all().length; + const new_akhys = (await userService.getAllUsers()).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})`, @@ -97,17 +83,17 @@ export async function getAkhys(client) { displayName: skin.displayName, contentTierUuid: skin.contentTierUuid, displayIcon: skin.displayIcon, - user_id: null, - tierRank: tier.rank, + userId: null, + tierRank: tier.rank != null ? String(tier.rank) : null, tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3", tierText: formatTierText(tier.rank, skin.displayName), basePrice: basePrice.toFixed(0), - maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0), + maxPrice: parseInt(calculateMaxPrice(basePrice, skin).toFixed(0)), }; }); if (skinsToInsert.length > 0) { - insertManySkins(skinsToInsert); + await skinService.insertManySkins(skinsToInsert); } console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`); } catch (err) { @@ -175,7 +161,7 @@ export function setupCronJobs(client, io) { cron.schedule(process.env.CRON_EXPR, async () => { console.log("[Cron] Running daily midnight tasks..."); try { - resetDailyReward.run(); + await userService.resetDailyReward(); console.log("[Cron] Daily rewards have been reset for all users."); //if (!getSOTD.get()) { initTodaysSOTD(); @@ -184,16 +170,16 @@ export function setupCronJobs(client, io) { console.error("[Cron] Error during daily reset:", e); } try { - const offers = getMarketOffers.all(); + const offers = await marketService.getMarketOffers(); const now = Date.now(); const TWO_DAYS = 2 * 24 * 60 * 60 * 1000; for (const offer of offers) { - if (now >= offer.closing_at + TWO_DAYS) { - const offerBids = getOfferBids.all(offer.id); + if (now >= offer.closingAt + TWO_DAYS) { + const offerBids = await marketService.getOfferBids(offer.id); for (const bid of offerBids) { - deleteBid.run(bid.id); + await marketService.deleteBid(bid.id); } - deleteMarketOffer.run(offer.id); + await marketService.deleteMarketOffer(offer.id); console.log(`[Cron] Deleted expired market offer ID: ${offer.id}`); } } @@ -207,14 +193,11 @@ export function setupCronJobs(client, io) { console.log("[Cron] Running daily 7 AM data sync..."); await getAkhys(client); try { - const akhys = getAllAkhys.all(); + const akhys = await userService.getAllAkhys(); 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 }), - }); + await userService.updateUserAvatar(akhy.id, user.displayAvatarURL({ dynamic: true, size: 256 })); } catch (err) { console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err); } @@ -276,51 +259,51 @@ export async function postAPOBuy(userId, amount) { // --- Miscellaneous Helpers --- -function handleMarketOffersUpdate() { +async function handleMarketOffersUpdate() { const now = Date.now(); - const offers = getMarketOffers.all(); + const offers = await marketService.getMarketOffers(); offers.forEach(async (offer) => { - if (now >= offer.opening_at && offer.status === "pending") { - updateMarketOffer.run({ id: offer.id, final_price: null, buyer_id: null, status: "open" }); + if (now >= offer.openingAt && offer.status === "pending") { + await marketService.updateMarketOffer({ id: offer.id, finalPrice: null, buyerId: null, status: "open" }); await handleMarketOfferOpening(offer.id, client); await emitMarketUpdate(); } - if (now >= offer.closing_at && offer.status !== "closed") { - const bids = getOfferBids.all(offer.id); + if (now >= offer.closingAt && offer.status !== "closed") { + const bids = await marketService.getOfferBids(offer.id); if (bids.length === 0) { // No bids placed, mark as closed without a sale - updateMarketOffer.run({ + await marketService.updateMarketOffer({ id: offer.id, - buyer_id: null, - final_price: null, + buyerId: null, + finalPrice: null, status: "closed", }); await emitMarketUpdate(); } else { const lastBid = bids[0]; - const seller = getUser.get(offer.seller_id); - const buyer = getUser.get(lastBid.bidder_id); + const seller = await userService.getUser(offer.sellerId); + const buyer = await userService.getUser(lastBid.bidderId); try { // Change skin ownership - const skin = getSkin.get(offer.skin_uuid); + const skin = await skinService.getSkin(offer.skinUuid); if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`); - updateSkin.run({ - user_id: buyer.id, + await skinService.updateSkin({ + userId: buyer.id, currentLvl: skin.currentLvl, currentChroma: skin.currentChroma, currentPrice: skin.currentPrice, uuid: skin.uuid, }); - updateMarketOffer.run({ + await marketService.updateMarketOffer({ id: offer.id, - buyer_id: buyer.id, - final_price: lastBid.offer_amount, + buyerId: buyer.id, + finalPrice: lastBid.offerAmount, status: "closed", }); - const newUserCoins = seller.coins + lastBid.offer_amount; - updateUserCoins.run({ id: seller.id, coins: newUserCoins }); + const newUserCoins = seller.coins + lastBid.offerAmount; + await userService.updateUserCoins(seller.id, newUserCoins); await emitMarketUpdate(); } catch (e) { console.error(`[Market Cron] Error processing offer ID: ${offer.id}`, e); diff --git a/src/utils/marketNotifs.js b/src/utils/marketNotifs.js index 6413b2d..72a156d 100644 --- a/src/utils/marketNotifs.js +++ b/src/utils/marketNotifs.js @@ -1,18 +1,20 @@ -import { getMarketOfferById, getOfferBids, getSkin, getUser } from "../database/index.js"; +import * as userService from "../services/user.service.js"; +import * as skinService from "../services/skin.service.js"; +import * as marketService from "../services/market.service.js"; import { EmbedBuilder } from "discord.js"; export async function handleNewMarketOffer(offerId, client) { - const offer = getMarketOfferById.get(offerId); + const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const skin = getSkin.get(offer.skin_uuid); + const skin = await skinService.getSkin(offer.skinUuid); - const discordUserSeller = await client.users.fetch(offer.seller_id); + const discordUserSeller = await client.users.fetch(offer.sellerId); try { - const userSeller = getUser.get(offer.seller_id); + const userSeller = await userService.getUser(offer.sellerId); if (discordUserSeller && userSeller?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Offre créée") - .setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été créée !`) + .setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été créée !`) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple .addFields( @@ -23,16 +25,16 @@ export async function handleNewMarketOffer(offerId, client) { }, { name: "💰 Prix de départ", - value: `\`${offer.starting_price} coins\``, + value: `\`${offer.startingPrice} coins\``, inline: true, }, { name: "⏰ Ouverture", - value: ``, + value: ``, }, { name: "⏰ Fermeture", - value: ``, + value: ``, }, { name: "🆔 ID de l’offre", @@ -53,26 +55,26 @@ export async function handleNewMarketOffer(offerId, client) { const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle offre") - .setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a été créée !`) + .setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a été créée !`) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple .addFields( { name: "💰 Prix de départ", - value: `\`${offer.starting_price} coins\``, + value: `\`${offer.startingPrice} coins\``, inline: true, }, { name: "⏰ Ouverture", - value: ``, + value: ``, }, { name: "⏰ Fermeture", - value: ``, + value: ``, }, { name: "Créée par", - value: `<@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`, + value: `<@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`, }, ) .setTimestamp(); @@ -83,18 +85,18 @@ export async function handleNewMarketOffer(offerId, client) { } export async function handleMarketOfferOpening(offerId, client) { - const offer = getMarketOfferById.get(offerId); + const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const skin = getSkin.get(offer.skin_uuid); + const skin = await skinService.getSkin(offer.skinUuid); try { - const discordUserSeller = await client.users.fetch(offer.seller_id); - const userSeller = getUser.get(offer.seller_id); + const discordUserSeller = await client.users.fetch(offer.sellerId); + const userSeller = await userService.getUser(offer.sellerId); if (discordUserSeller && userSeller?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Début des enchères") .setDescription( - `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`, + `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple @@ -106,12 +108,12 @@ export async function handleMarketOfferOpening(offerId, client) { }, { name: "💰 Prix de départ", - value: `\`${offer.starting_price} coins\``, + value: `\`${offer.startingPrice} coins\``, inline: true, }, { name: "⏰ Fermeture", - value: ``, + value: ``, }, { name: "🆔 ID de l’offre", @@ -133,19 +135,19 @@ export async function handleMarketOfferOpening(offerId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Début des enchères") .setDescription( - `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`, + `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple .addFields( { name: "💰 Prix de départ", - value: `\`${offer.starting_price} coins\``, + value: `\`${offer.startingPrice} coins\``, inline: true, }, { name: "⏰ Fermeture", - value: ``, + value: ``, }, ) .setTimestamp(); @@ -156,19 +158,19 @@ export async function handleMarketOfferOpening(offerId, client) { } export async function handleMarketOfferClosing(offerId, client) { - const offer = getMarketOfferById.get(offerId); + const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const skin = getSkin.get(offer.skin_uuid); - const bids = getOfferBids.all(offer.id); + const skin = await skinService.getSkin(offer.skinUuid); + const bids = await marketService.getOfferBids(offer.id); - const discordUserSeller = await client.users.fetch(offer.seller_id); + const discordUserSeller = await client.users.fetch(offer.sellerId); try { - const userSeller = getUser.get(offer.seller_id); + const userSeller = await userService.getUser(offer.sellerId); if (discordUserSeller && userSeller?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Fin des enchères") .setDescription( - `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, + `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple @@ -188,11 +190,11 @@ export async function handleMarketOfferClosing(offerId, client) { ); } else { const highestBid = bids[0]; - const highestBidderUser = await client.users.fetch(highestBid.bidder_id); + const highestBidderUser = await client.users.fetch(highestBid.bidderId); embed.addFields( { name: "✅ Enchères terminées avec succès !", - value: `Ton skin a été vendu pour \`${highestBid.offer_amount} coins\` à <@${highestBid.bidder_id}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, + value: `Ton skin a été vendu pour \`${highestBid.offerAmount} coins\` à <@${highestBid.bidderId}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, }, { name: "🆔 ID de l’offre", @@ -216,7 +218,7 @@ export async function handleMarketOfferClosing(offerId, client) { const embed = new EmbedBuilder() .setTitle("🔔 Fin des enchères") .setDescription( - `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, + `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple @@ -229,18 +231,18 @@ export async function handleMarketOfferClosing(offerId, client) { }); } else { const highestBid = bids[0]; - const highestBidderUser = await client.users.fetch(highestBid.bidder_id); + const highestBidderUser = await client.users.fetch(highestBid.bidderId); embed.addFields({ name: "✅ Enchères terminées avec succès !", - value: `Le skin de <@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""} a été vendu pour \`${highestBid.offer_amount} coins\` à <@${highestBid.bidder_id}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, + value: `Le skin de <@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""} a été vendu pour \`${highestBid.offerAmount} coins\` à <@${highestBid.bidderId}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, }); - const discordUserBidder = await client.users.fetch(highestBid.bidder_id); - const userBidder = getUser.get(highestBid.bidder_id); + const discordUserBidder = await client.users.fetch(highestBid.bidderId); + const userBidder = await userService.getUser(highestBid.bidderId); if (discordUserBidder && userBidder?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Fin des enchères") .setDescription( - `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, + `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple @@ -248,7 +250,7 @@ export async function handleMarketOfferClosing(offerId, client) { const highestBid = bids[0]; embed.addFields({ name: "✅ Enchères terminées avec succès !", - value: `Tu as acheté ce skin pour \`${highestBid.offer_amount} coins\` à <@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}. Il a été ajouté à ton inventaire.`, + value: `Tu as acheté ce skin pour \`${highestBid.offerAmount} coins\` à <@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}. Il a été ajouté à ton inventaire.`, }); discordUserBidder.send({ embeds: [embed] }).catch(console.error); @@ -262,39 +264,39 @@ export async function handleMarketOfferClosing(offerId, client) { export async function handleNewMarketOfferBid(offerId, bidId, client) { // Notify Seller and Bidder - const offer = getMarketOfferById.get(offerId); + const offer = await marketService.getMarketOfferById(offerId); if (!offer) return; - const bid = getOfferBids.get(offerId); + const bid = (await marketService.getOfferBids(offerId))[0]; if (!bid) return; - const skin = getSkin.get(offer.skin_uuid); + const skin = await skinService.getSkin(offer.skinUuid); - const bidderUser = client.users.fetch(bid.bidder_id); + const bidderUser = client.users.fetch(bid.bidderId); try { - const discordUserSeller = await client.users.fetch(offer.seller_id); - const userSeller = getUser.get(offer.seller_id); + const discordUserSeller = await client.users.fetch(offer.sellerId); + const userSeller = await userService.getUser(offer.sellerId); if (discordUserSeller && userSeller?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle enchère") .setDescription( - `Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**.`, + `Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**.`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple .addFields( { name: "👤 Enchérisseur", - value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, + value: `<@${bid.bidderId}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, inline: true, }, { name: "💰 Montant de l’enchère", - value: `\`${bid.offer_amount} coins\``, + value: `\`${bid.offerAmount} coins\``, inline: true, }, { name: "⏰ Fermeture", - value: ``, + value: ``, }, { name: "🆔 ID de l’offre", @@ -311,19 +313,19 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { } try { - const discordUserNewBidder = await client.users.fetch(bid.bidder_id); - const userNewBidder = getUser.get(bid.bidder_id); + const discordUserNewBidder = await client.users.fetch(bid.bidderId); + const userNewBidder = await userService.getUser(bid.bidderId); if (discordUserNewBidder && userNewBidder?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle enchère") .setDescription( - `Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été placée!`, + `Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été placée!`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple .addFields({ name: "💰 Montant de l’enchère", - value: `\`${bid.offer_amount} coins\``, + value: `\`${bid.offerAmount} coins\``, inline: true, }) .setTimestamp(); @@ -335,28 +337,28 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { } try { - const offerBids = getOfferBids.all(offer.id); + const offerBids = await marketService.getOfferBids(offer.id); if (offerBids.length < 2) return; // No previous bidder to notify - const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidder_id); - const userPreviousBidder = getUser.get(offerBids[1].bidder_id); + const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidderId); + const userPreviousBidder = await userService.getUser(offerBids[1].bidderId); if (discordUserPreviousBidder && userPreviousBidder?.isAkhy) { const embed = new EmbedBuilder() .setTitle("🔔 Nouvelle enchère") .setDescription( - `Quelqu'un a surenchéri sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**, tu n'es plus le meilleur enchérisseur !`, + `Quelqu'un a surenchéri sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**, tu n'es plus le meilleur enchérisseur !`, ) .setThumbnail(skin.displayIcon) .setColor(0x5865f2) // Discord blurple .addFields( { name: "👤 Enchérisseur", - value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, + value: `<@${bid.bidderId}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, inline: true, }, { name: "💰 Montant de l’enchère", - value: `\`${bid.offer_amount} coins\``, + value: `\`${bid.offerAmount} coins\``, inline: true, }, ) @@ -373,7 +375,7 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) { export async function handleCaseOpening(caseType, userId, skinUuid, client) { const discordUser = await client.users.fetch(userId); - const skin = getSkin.get(skinUuid); + const skin = await skinService.getSkin(skinUuid); try { const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); const embed = new EmbedBuilder()