diff --git a/.env.example b/.env.example index 1b7bc24..cf84bd6 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,9 @@ NODE_ENV=test +LOCATION= +BOT_OWNER= DISCORD_GUILD_ID= SHINUWA_USER_ID= +INVITE_LINK= WEB_SERVER_PORT=3000 WEB_API_KEY= DISCORD_APPLICATION_ID= diff --git a/.gitignore b/.gitignore index d9ecbe6..280a034 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,14 @@ -# Node.js +#node node_modules/ -coverage/ -*.log - -# Environment files, including local variants and backups -.env* -!.env.example # Local configuration and test data +.env +.env.* +!.env.example src/static/gameservers-list.json -# Runtime storage -storage/runtime/ +#storage storage/permanent/* !storage/permanent/.gitkeep storage/temp/* !storage/temp/.gitkeep - -# Operating system files -.DS_Store diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index a45fd52..0000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -24 diff --git a/README.md b/README.md index c0243d7..0e6dc6e 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,6 @@ fichier `.env` avec les valeurs appropriées. Le fichier `.env.example` documente les variables attendues sans contenir de secret. Les fichiers réels doivent rester hors du dépôt. -Générer une clé d'API robuste avec : - -```bash -openssl rand -hex 32 -``` - ```bash # Lancer le bot npm start @@ -25,40 +19,3 @@ npm run deploy Node.js 22.12 ou supérieur est nécessaire pour la prise en charge du protocole vocal DAVE de Discord. - -## Prérequis - -- Node.js 24 LTS ; -- FFmpeg et FFprobe disponibles dans le `PATH` ; -- accès UDP sortant vers les serveurs vocaux Discord. - -Sur Debian ou Ubuntu : - -```bash -sudo apt install ffmpeg -npm ci -npm run check -``` - -## Données persistantes - -Le dossier `storage/` est une donnée applicative et ne doit pas être remplacé -pendant un déploiement. Sauvegarder régulièrement : - -- `storage/runtime/library.json` ; -- `storage/permanent/`. - -Les fichiers placés dans `storage/temp/` sont temporaires et nettoyés après -24 heures. - -La liste des serveurs de jeux est locale. La créer à partir du modèle : - -```bash -cp src/static/gameservers-list.example.json src/static/gameservers-list.json -``` - -## Déploiement - -Un push sur `main` exécute les contrôles syntaxiques, les tests et l'audit npm -avant d'appeler `/usr/local/bin/deploy-palico-bot` sur le serveur. Le workflow -vérifie ensuite la route publique `/healthz`. diff --git a/deployCommands.js b/deployCommands.js index db46678..854ab0a 100644 --- a/deployCommands.js +++ b/deployCommands.js @@ -50,8 +50,7 @@ const rest = new REST().setToken(config.token); console.log( `Successfully reloaded ${data.length} application (/) commands.` ); - } catch (error) { - console.error(error); - process.exitCode = 1; - } -})(); + } catch (error) { + console.error(error); + } +})(); diff --git a/index.js b/index.js index 7af0634..7c5a3af 100644 --- a/index.js +++ b/index.js @@ -56,36 +56,9 @@ for (const file of eventFiles) { } } +client.login(config.token); + const musicQueue = new MusicQueue(client); -await client.login(config.token); - -const { server } = startWebServer({ queue: musicQueue }); - -server.on("error", (error) => { - console.error("HTTP server error", error); -}); - -let shuttingDown = false; -async function shutdown(signal) { - if (shuttingDown) return; - shuttingDown = true; - console.log(`[${new Date().toLocaleString()}] Shutting down (${signal})`); - - musicQueue.shutdown(); - client.destroy(); - - const forceExit = setTimeout(() => process.exit(1), 10_000); - forceExit.unref(); - - server.close(() => { - clearTimeout(forceExit); - }); -} - -process.once("SIGTERM", () => void shutdown("SIGTERM")); -process.once("SIGINT", () => void shutdown("SIGINT")); -process.on("unhandledRejection", (error) => { - console.error("Unhandled promise rejection", error); -}); +startWebServer({ queue: musicQueue }); export { client, musicQueue }; diff --git a/package-lock.json b/package-lock.json index 4f17970..42b8e3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,27 +10,27 @@ "license": "ISC", "dependencies": { "@discordjs/voice": "^0.19.2", - "discord.js": "^14.26.4", - "express": "^5.2.1", - "express-rate-limit": "^8.5.2", - "helmet": "^8.2.0", - "multer": "^2.2.0", - "opusscript": "^0.0.8" + "discord.js": "^14.18.0", + "express": "^5.1.0", + "libsodium-wrappers": "^0.7.15", + "multer": "^1.4.5-lts.1", + "opusscript": "^0.0.8", + "uuid": "^11.0.3" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@discordjs/builders": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", - "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.6.2", - "@discordjs/util": "^1.2.0", + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", - "discord-api-types": "^0.38.40", + "discord-api-types": "^0.38.31", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" @@ -52,12 +52,12 @@ } }, "node_modules/@discordjs/formatters": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", - "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", "license": "Apache-2.0", "dependencies": { - "discord-api-types": "^0.38.33" + "discord-api-types": "^0.38.1" }, "engines": { "node": ">=16.11.0" @@ -67,20 +67,20 @@ } }, "node_modules/@discordjs/rest": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", - "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^2.1.1", - "@discordjs/util": "^1.2.0", + "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", - "@sapphire/snowflake": "^3.5.5", + "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", - "discord-api-types": "^0.38.40", - "magic-bytes.js": "^1.13.0", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", - "undici": "6.24.1" + "undici": "6.21.3" }, "engines": { "node": ">=18" @@ -101,24 +101,11 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", - "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", - "license": "MIT", - "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" - } - }, "node_modules/@discordjs/util": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", - "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", "license": "Apache-2.0", - "dependencies": { - "discord-api-types": "^0.38.33" - }, "engines": { "node": ">=18" }, @@ -588,40 +575,23 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/buffer-from": { @@ -680,20 +650,50 @@ } }, "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "engines": [ - "node >= 6.0" + "node >= 0.8" ], "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^3.0.2", + "readable-stream": "^2.2.2", "typedarray": "^0.0.6" } }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -733,6 +733,12 @@ "node": ">=6.6.0" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -769,24 +775,24 @@ ] }, "node_modules/discord.js": { - "version": "14.26.4", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz", - "integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==", + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.14.1", + "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", - "@discordjs/formatters": "^0.6.2", - "@discordjs/rest": "^2.6.1", - "@discordjs/util": "^1.2.0", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", - "discord-api-types": "^0.38.40", + "discord-api-types": "^0.38.31", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", - "magic-bytes.js": "^1.13.0", + "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", - "undici": "6.24.1" + "undici": "6.21.3" }, "engines": { "node": ">=18" @@ -843,9 +849,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -870,19 +876,18 @@ } }, "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.1", + "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", - "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -912,24 +917,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1042,9 +1029,9 @@ } }, "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1053,52 +1040,41 @@ "node": ">= 0.4" } }, - "node_modules/helmet": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", - "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/EvanHahn" - } - }, "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/inherits": { @@ -1107,15 +1083,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1131,10 +1098,31 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/libsodium": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.7.15" + } + }, "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, "node_modules/lodash.snakecase": { @@ -1144,9 +1132,9 @@ "license": "MIT" }, "node_modules/magic-bytes.js": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", - "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", "license": "MIT" }, "node_modules/math-intrinsics": { @@ -1200,6 +1188,15 @@ "node": ">= 0.6" } }, + "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/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1207,22 +1204,22 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz", - "integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==", + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", "license": "MIT", "dependencies": { "append-field": "^1.0.0", - "busboy": "^1.6.0", - "concat-stream": "^2.0.0", - "type-is": "^1.6.18" + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" }, "engines": { - "node": ">= 10.16.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 6.0.0" } }, "node_modules/multer/node_modules/media-typer": { @@ -1255,6 +1252,18 @@ "node": ">= 0.6" } }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/multer/node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1277,6 +1286,15 @@ "node": ">= 0.6" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1326,9 +1344,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "license": "MIT", "funding": { "type": "opencollective", @@ -1361,6 +1379,12 @@ } } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1375,13 +1399,12 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { - "es-define-property": "^1.0.1", - "side-channel": "^1.1.1" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -1400,32 +1423,34 @@ } }, "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" }, "engines": { "node": ">= 0.10" } }, - "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/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/router": { @@ -1514,14 +1539,14 @@ "license": "ISC" }, "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -1533,13 +1558,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" @@ -1602,15 +1627,6 @@ "node": ">=10.0.0" } }, - "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/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1633,34 +1649,17 @@ "license": "0BSD" }, "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "content-type": "^2.0.0", + "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, "node_modules/typedarray": { @@ -1670,9 +1669,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.27.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", - "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", "license": "MIT", "engines": { "node": ">=18.17" @@ -1699,6 +1698,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1734,6 +1746,15 @@ "optional": true } } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } } } } diff --git a/package.json b/package.json index dac397d..e79c54e 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,26 @@ { "name": "palico-bot", "version": "1.0.0", - "private": true, - "description": "Bot Discord et jukebox web autohébergé", "main": "index.js", "type": "module", "scripts": { "start": "node --env-file=.env index.js", - "deploy": "node --env-file=.env deployCommands.js", - "check:syntax": "node scripts/check-syntax.js", - "test": "node --test --test-concurrency=1", - "check": "npm run check:syntax && npm test" + "deploy": "node --env-file=.env deployCommands.js" }, "engines": { "node": ">=22.12.0" }, + "keywords": [], + "author": "", "license": "ISC", + "description": "", "dependencies": { "@discordjs/voice": "^0.19.2", - "discord.js": "^14.26.4", - "express": "^5.2.1", - "express-rate-limit": "^8.5.2", - "helmet": "^8.2.0", - "multer": "^2.2.0", - "opusscript": "^0.0.8" - }, - "overrides": { - "undici": "^6.27.0" + "discord.js": "^14.18.0", + "express": "^5.1.0", + "libsodium-wrappers": "^0.7.15", + "multer": "^1.4.5-lts.1", + "opusscript": "^0.0.8", + "uuid": "^11.0.3" } } diff --git a/public/jukebox.html b/public/jukebox.html index b7990d7..359b84e 100644 --- a/public/jukebox.html +++ b/public/jukebox.html @@ -31,7 +31,7 @@

Authentification

- La clé reste dans cet onglet jusqu'à sa fermeture. + Ca sera stocké sur ton navigateur si tu sauvegardes !

@@ -42,17 +42,17 @@ id="displayName" name="displayName" placeholder="Shinuwa" - autocomplete="nickname" + autocomplete="off" />
@@ -171,7 +171,7 @@ diff --git a/public/scripts/common.js b/public/scripts/common.js index f73a60d..241c5d4 100644 --- a/public/scripts/common.js +++ b/public/scripts/common.js @@ -2,11 +2,11 @@ const API_KEY_STORAGE_KEY = "palico-api-key"; const DISPLAY_NAME_STORAGE_KEY = "palico-display-name"; export function getStoredApiKey() { - return sessionStorage.getItem(API_KEY_STORAGE_KEY) || ""; + return localStorage.getItem(API_KEY_STORAGE_KEY) || ""; } export function setStoredApiKey(value) { - sessionStorage.setItem(API_KEY_STORAGE_KEY, value || ""); + localStorage.setItem(API_KEY_STORAGE_KEY, value || ""); } export function getStoredDisplayName() { diff --git a/public/scripts/jukebox.js b/public/scripts/jukebox.js index ff3c8d8..10c64d8 100644 --- a/public/scripts/jukebox.js +++ b/public/scripts/jukebox.js @@ -41,11 +41,9 @@ const elements = { heroQueueBtn: document.getElementById("heroQueueBtn"), uploadSection: document.getElementById("uploadSection"), queueSection: document.getElementById("queueSection"), - backHomeBtn: document.getElementById("backHomeBtn"), }; let pollIntervalId = null; -let refreshPromise = null; const { setStatus, clearStatus } = createStatusManager(elements.status); const apiFetch = createApiClient({ @@ -53,18 +51,7 @@ const apiFetch = createApiClient({ getApiKey: () => state.apiKey, }); -function refreshAll(options = {}) { - if (refreshPromise) { - return refreshPromise; - } - - refreshPromise = performRefresh(options).finally(() => { - refreshPromise = null; - }); - return refreshPromise; -} - -async function performRefresh({ silent = false } = {}) { +async function refreshAll({ silent = false } = {}) { if (!silent) { clearStatus(); } @@ -306,9 +293,6 @@ function bindEvents() { elements.heroQueueBtn?.addEventListener("click", () => elements.queueSection?.scrollIntoView({ behavior: "smooth" }) ); - elements.backHomeBtn?.addEventListener("click", () => { - window.location.href = "/"; - }); elements.playerButtons?.forEach((button) => { button.addEventListener("click", async () => { diff --git a/public/style/styles.scss b/public/style/styles.scss new file mode 100644 index 0000000..b8e2f7f --- /dev/null +++ b/public/style/styles.scss @@ -0,0 +1,417 @@ +:root { + color-scheme: dark; + --bg: #06030d; + --bg-card: rgba(22, 14, 36, 0.9); + --bg-glass: rgba(42, 25, 66, 0.65); + --border: rgba(255, 255, 255, 0.08); + --accent: #b259ff; + --accent-strong: #d66bff; + --muted: #cbc4d5; + --muted-dark: #8c7ea7; + --danger: #ff4d6d; + --success: #59ffa7; + --gradient: radial-gradient(circle at 20% 20%, #2d1a49, transparent 45%), + radial-gradient(circle at 80% 0%, #551e8c, transparent 40%), + radial-gradient(circle at 50% 80%, #150b28, transparent 30%); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--gradient), #050109; + color: #f7f1ff; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem 4rem; +} + +h1, +h2, +h3, +h4 { + margin: 0; +} + +p { + margin-top: 0; +} + +.text { + &--muted { + color: var(--muted); + text-align: center; + } +} + +.hero { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 2rem; + padding: 3rem 1.5rem; + max-width: 1200px; + margin: 0 auto; + + &--home { + flex-direction: row; + align-items: center; + text-align: left; + gap: 2.75rem; + + .hero__visual img { + max-width: 320px; + } + } + + &__content { + flex: 1; + min-width: 280px; + + h1 { + font-size: 2.2rem; + line-height: 1.1; + margin-top: 1rem; + margin-bottom: 1rem; + } + + p { + color: var(--muted); + font-size: 1.05rem; + max-width: 500px; + } + } + + &__actions { + display: flex; + gap: 1rem; + margin-top: 1.5rem; + + &--centered { + justify-content: center; + margin: 2rem auto 3rem; + } + } + + &__visual { + min-width: 200px; + display: flex; + justify-content: center; + + img { + max-width: 200px; + width: 100%; + filter: drop-shadow(0 25px 45px rgba(0, 0, 0, 0.45)); + } + + &--large img { + max-width: 270px; + } + } + + &__auth { + max-width: 300px; + } +} + +.pill { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-size: 0.9rem; + padding: 0.35rem 0.85rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: var(--muted); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.panel { + border-radius: 18px; + padding: 1.75rem; + margin-bottom: 1.75rem; + border: 1px solid var(--border); + + &--glass { + background: var(--bg-glass); + box-shadow: 0 20px 38px rgba(2, 0, 8, 0.45); + backdrop-filter: blur(18px); + } + + &__subtitle { + color: var(--muted); + margin-top: 0.35rem; + margin-bottom: 1.5rem; + } + + &__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.25rem; + } +} + +.badge { + padding: 0.35rem 0.9rem; + background: rgba(178, 89, 255, 0.2); + color: var(--accent); + border-radius: 12px; + font-size: 0.85rem; +} + +label { + display: block; + font-size: 0.95rem; + color: var(--muted); + margin-bottom: 0.35rem; +} + +input[type="text"], +input[type="file"], +select, +textarea { + width: 100%; + padding: 0.7rem 0.9rem; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-size: 1rem; +} + +input[type="file"] { + padding: 0.5rem; +} + +input:focus, +textarea:focus, +select:focus { + outline: 2px solid rgba(178, 89, 255, 0.5); + border-color: rgba(178, 89, 255, 0.6); +} + +.form-checkbox { + display: flex; + align-items: flex-start; + gap: 0.5rem; + color: var(--muted); + + &__input { + margin-top: 0.25rem; + } +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.65rem 1.55rem; + font-weight: 600; + cursor: pointer; + background: linear-gradient(120deg, var(--accent), var(--accent-strong)); + color: #08010f; + transition: transform 0.15s ease, box-shadow 0.15s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(178, 89, 255, 0.35); + } + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + box-shadow: none; + transform: none; + } + + &--secondary { + background: rgba(255, 255, 255, 0.15); + color: #fff; + } + + &--danger { + background: linear-gradient(120deg, #ff4d6d, #ff89a5); + color: #fff; + } + + &--ghost { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.35); + color: #fff; + } + + &--small { + padding: 0.4rem 1rem; + font-size: 0.9rem; + } +} + +.control-group { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.25rem; +} + +.grid { + display: grid; + gap: 1rem; + + &--two { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + } +} + +.u-mt-1 { + margin-top: 1rem; +} + +.form-actions { + margin-top: 1.25rem; + display: flex; + justify-content: flex-end; +} + +.status { + padding: 0.85rem 1.25rem; + border-radius: 999px; + font-size: 0.95rem; + margin: 1rem 0 2rem; + max-width: 600px; + + &--info { + background: rgba(178, 89, 255, 0.2); + color: var(--accent); + } + + &--error { + background: rgba(255, 77, 109, 0.15); + color: var(--danger); + } + + &--success { + background: rgba(89, 255, 167, 0.15); + color: var(--success); + } +} + +.list { + list-style: none; + padding: 0; + margin: 0; + + &--stack > li { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } +} + +.media-item { + display: flex; + gap: 1rem; + justify-content: space-between; + align-items: center; + padding: 0.9rem 0; + + &__info { + flex: 1; + } + + &__title { + font-weight: 600; + font-size: 1rem; + } + + &__meta { + color: var(--muted); + font-size: 0.9rem; + } + + &__actions { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } +} + +.player-current { + background: rgba(0, 0, 0, 0.25); + border-radius: 16px; + padding: 1.2rem; + border: 1px solid rgba(255, 255, 255, 0.08); + + &--empty { + color: var(--muted); + text-align: center; + } +} + +.feature-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + + &__card { + text-decoration: none; + color: inherit; + display: block; + border: 1px solid var(--border); + border-radius: 18px; + padding: 1.25rem 1.5rem; + background: var(--bg-card); + transition: transform 0.15s ease, box-shadow 0.15s ease; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 15px 30px rgba(2, 0, 8, 0.45); + } + + .pill { + margin-bottom: 0.5rem; + } + } +} + +@media (max-width: 900px) { + .hero, + .hero--home { + flex-direction: column; + text-align: center; + } + + .hero__content p { + margin: 0 auto; + } + + .hero__actions { + justify-content: center; + } + + .panel__header { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 600px) { + .btn, + input[type="text"], + input[type="file"], + select, + textarea { + width: 100%; + } + + .control-group { + flex-direction: column; + } +} diff --git a/scripts/check-syntax.js b/scripts/check-syntax.js deleted file mode 100644 index a50c2a7..0000000 --- a/scripts/check-syntax.js +++ /dev/null @@ -1,31 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { spawnSync } from "node:child_process"; - -const ROOTS = ["index.js", "deployCommands.js", "src", "public/scripts", "scripts", "test"]; - -function collectJavaScriptFiles(entry) { - const absolutePath = path.resolve(entry); - const stat = fs.statSync(absolutePath); - if (stat.isFile()) { - return absolutePath.endsWith(".js") ? [absolutePath] : []; - } - - return fs - .readdirSync(absolutePath, { withFileTypes: true }) - .flatMap((child) => - collectJavaScriptFiles(path.join(absolutePath, child.name)) - ); -} - -const files = ROOTS.flatMap(collectJavaScriptFiles); -for (const file of files) { - const result = spawnSync(process.execPath, ["--check", file], { - stdio: "inherit", - }); - if (result.status !== 0) { - process.exit(result.status || 1); - } -} - -console.log(`Syntax checked: ${files.length} JavaScript files`); diff --git a/src/commands/game/dice.js b/src/commands/game/dice.js index e9257fb..0bf8d03 100644 --- a/src/commands/game/dice.js +++ b/src/commands/game/dice.js @@ -8,19 +8,15 @@ const command = { .setDescription("Lance un ou plusieurs dés") .addIntegerOption((option) => option - .setName("number") - .setDescription("Nombre de dés a lancer") - .setMinValue(1) - .setMaxValue(20) - .setRequired(true) + .setName("number") + .setDescription("Nombre de dés a lancer") + .setRequired(true) ) .addIntegerOption((option) => option - .setName("faces") - .setDescription("Nombre de faces sur le dé") - .setMinValue(2) - .setMaxValue(1000) - .setRequired(true) + .setName("faces") + .setDescription("Nombre de faces sur le dé") + .setRequired(true) ), async execute(interaction) { const number = interaction.options.getInteger("number"); @@ -30,6 +26,14 @@ const command = { var tails = 0; var heads = 0; + if (faces === 1 || faces === 0) { + await interaction.reply({ + content: "Ouais ouais, bien sur. Allez, bisous !", + ephemeral: true, + }); + return; + } + if (faces === 2) { message = message + subtext(`Pile = 1 et Face = 2\n`); } @@ -43,7 +47,7 @@ const command = { sum = sum + random; message = message + `[ ${random} ] ${i === number - 1 ? "\n" : " "}`; - await interaction.editReply(message); + interaction.editReply(message); } if (number > 1 && faces > 2) { @@ -60,7 +64,7 @@ const command = { ); } - await interaction.editReply(message); + interaction.editReply(message); }, }; diff --git a/src/commands/game/maketeam.js b/src/commands/game/maketeam.js index fb6c761..90f4ce4 100644 --- a/src/commands/game/maketeam.js +++ b/src/commands/game/maketeam.js @@ -6,42 +6,23 @@ const command = { .setDescription("Crée des equipes au hasard") .addIntegerOption((option) => option - .setName("number") - .setDescription("Nombre d'équipe") - .setMinValue(1) - .setMaxValue(20) - .setRequired(true) + .setName("number") + .setDescription("Nombre d'équipe") + .setRequired(true) ) .addStringOption((option) => option - .setName("players") - .setDescription("Liste des joueurs, séparés par un espace") - .setMinLength(1) - .setMaxLength(1000) - .setRequired(true) + .setName("players") + .setDescription("Liste des joueurs, séparés par un espace") + .setRequired(true) ), async execute(interaction) { const teamNumber = interaction.options.getInteger("number"); const players = interaction.options.getString("players"); - const playersArray = players.trim().split(/\s+/); - - if (teamNumber > playersArray.length) { - await interaction.reply({ - content: "Le nombre d'équipes ne peut pas dépasser le nombre de joueurs.", - ephemeral: true, - }); - return; - } - - const teams = Array.from({ length: teamNumber }, () => []); - const shuffledPlayers = [...playersArray]; - for (let index = shuffledPlayers.length - 1; index > 0; index--) { - const randomIndex = Math.floor(Math.random() * (index + 1)); - [shuffledPlayers[index], shuffledPlayers[randomIndex]] = [ - shuffledPlayers[randomIndex], - shuffledPlayers[index], - ]; - } + const playersArray = players.split(" "); + + const teams = Array.from({ length: teamNumber }, () => []); + const shuffledPlayers = [...playersArray].sort(() => Math.random() - 0.5); shuffledPlayers.forEach((player, index) => { const teamIndex = index % teamNumber; diff --git a/src/commands/utility/deletemsgs.js b/src/commands/utility/deletemsgs.js index dc7a194..c9403d5 100644 --- a/src/commands/utility/deletemsgs.js +++ b/src/commands/utility/deletemsgs.js @@ -1,32 +1,31 @@ -import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; -import { requirePermission } from "../../features/permissions.js"; +import { SlashCommandBuilder } from "discord.js"; const command = { data: new SlashCommandBuilder() - .setName("deletemsgs") - .setDescription("Supprime tous les messages, sauf ceux épinglés") - .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages), - async execute(interaction) { - if (!(await requirePermission(interaction, PermissionFlagsBits.ManageMessages))) { - return; - } - - const textChannel = interaction.channel; - if (!textChannel?.isTextBased() || !textChannel.messages) { - await interaction.reply({ - content: "Cette commande doit être utilisée dans un salon textuel.", - ephemeral: true, - }); - return; - } - - await interaction.deferReply({ ephemeral: true }); - const fetched = await textChannel.messages.fetch({ limit: 100 }); - const notPinned = fetched.filter((message) => !message.pinned); - const deleted = await textChannel.bulkDelete(notPinned, true); - - await interaction.editReply(`Et voilà, ${deleted.size} message(s) supprimé(s) ! 🐱`); - }, -}; + .setName("deletemsgs") + .setDescription("Supprime tous les messages, sauf ceux épinglés"), + async execute(interaction) { + let textChannel = interaction.client.channels.cache.get( + interaction.channelId + ); + + textChannel.messages + .fetch({ limit: 100 }) + .then((fetched) => { + const notPinned = fetched.filter((fetchedMsg) => !fetchedMsg.pinned); + textChannel.bulkDelete(notPinned, true); + console.log( + `[${new Date().toLocaleString()}] Command : Delete Messages used` + ); + }) + .catch(console.error); + + interaction.reply("Et voila, c'est tout propre ! 🐱").then(() => { + setTimeout(() => { + interaction.deleteReply(); + }, 7000); + }); + }, +}; export { command }; diff --git a/src/commands/utility/ping.js b/src/commands/utility/ping.js index 46190ad..50fc710 100644 --- a/src/commands/utility/ping.js +++ b/src/commands/utility/ping.js @@ -1,11 +1,12 @@ import { SlashCommandBuilder } from "discord.js"; const command = { - data: new SlashCommandBuilder() + data: new SlashCommandBuilder() .setName("ping") .setDescription("Reponds avec Pong"), - async execute(interaction) { - await interaction.reply({ content: "Euh... Pong ! 🐱", ephemeral: true }); + async execute(interaction) { + console.log(interaction); + await interaction.reply({ content: "Euh... Pong ! 🐱", ephemeral: true }); }, }; diff --git a/src/commands/vocal/deafall.js b/src/commands/vocal/deafall.js index 9ec7ec5..7c8bcbe 100644 --- a/src/commands/vocal/deafall.js +++ b/src/commands/vocal/deafall.js @@ -1,15 +1,12 @@ -import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; -import { changeVoiceStatus } from "../../features/utilities.js"; -import { requirePermission } from "../../features/permissions.js"; +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; const command = { data: new SlashCommandBuilder() - .setName("deafall") - .setDescription("Deaf tout le monde dans le lounge") - .setDefaultMemberPermissions(PermissionFlagsBits.DeafenMembers), - async execute(interaction) { - if (!(await requirePermission(interaction, PermissionFlagsBits.DeafenMembers))) return; - await changeVoiceStatus(interaction.client, "deaf", "all"); + .setName("deafall") + .setDescription("Deaf tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "deaf", "all"); await interaction.reply("On n'entends plus rien ! 🐱"); }, }; diff --git a/src/commands/vocal/muteall.js b/src/commands/vocal/muteall.js index 86f5dda..33a6005 100644 --- a/src/commands/vocal/muteall.js +++ b/src/commands/vocal/muteall.js @@ -1,15 +1,12 @@ -import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; -import { changeVoiceStatus } from "../../features/utilities.js"; -import { requirePermission } from "../../features/permissions.js"; +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; const command = { data: new SlashCommandBuilder() - .setName("muteall") - .setDescription("Mute tout le monde dans le lounge") - .setDefaultMemberPermissions(PermissionFlagsBits.MuteMembers), - async execute(interaction) { - if (!(await requirePermission(interaction, PermissionFlagsBits.MuteMembers))) return; - await changeVoiceStatus(interaction.client, "mute", "all"); + .setName("muteall") + .setDescription("Mute tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "mute", "all"); await interaction.reply("Voila, tout le monde est mioute ! 🐱"); }, }; diff --git a/src/commands/vocal/undeafall.js b/src/commands/vocal/undeafall.js index 878b82d..fa60861 100644 --- a/src/commands/vocal/undeafall.js +++ b/src/commands/vocal/undeafall.js @@ -1,15 +1,12 @@ -import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; -import { changeVoiceStatus } from "../../features/utilities.js"; -import { requirePermission } from "../../features/permissions.js"; +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; const command = { data: new SlashCommandBuilder() - .setName("undeafall") - .setDescription("Undeaf tout le monde dans le lounge") - .setDefaultMemberPermissions(PermissionFlagsBits.DeafenMembers), - async execute(interaction) { - if (!(await requirePermission(interaction, PermissionFlagsBits.DeafenMembers))) return; - await changeVoiceStatus(interaction.client, "undeaf", "all"); + .setName("undeafall") + .setDescription("Undeaf tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "undeaf", "all"); await interaction.reply("Voila, les oreilles sont réparées ! 🐱"); }, }; diff --git a/src/commands/vocal/unmuteall.js b/src/commands/vocal/unmuteall.js index cb83668..ba9f4ed 100644 --- a/src/commands/vocal/unmuteall.js +++ b/src/commands/vocal/unmuteall.js @@ -1,15 +1,12 @@ -import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js"; -import { changeVoiceStatus } from "../../features/utilities.js"; -import { requirePermission } from "../../features/permissions.js"; +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; const command = { data: new SlashCommandBuilder() - .setName("unmuteall") - .setDescription("Unmute tout le monde dans le lounge") - .setDefaultMemberPermissions(PermissionFlagsBits.MuteMembers), - async execute(interaction) { - if (!(await requirePermission(interaction, PermissionFlagsBits.MuteMembers))) return; - await changeVoiceStatus(interaction.client, "unmute", "all"); + .setName("unmuteall") + .setDescription("Unmute tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "unmute", "all"); await interaction.reply("Et.... unmioute ! 🐱"); }, }; diff --git a/src/config.js b/src/config.js index 4aa7883..6ddb3af 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,10 @@ const requiredVariables = [ "NODE_ENV", + "LOCATION", + "BOT_OWNER", "DISCORD_GUILD_ID", "SHINUWA_USER_ID", + "INVITE_LINK", "WEB_SERVER_PORT", "WEB_API_KEY", "DISCORD_APPLICATION_ID", @@ -23,18 +26,17 @@ if (missingVariables.length > 0) { const webServerPort = Number(process.env.WEB_SERVER_PORT); -if ( - !Number.isInteger(webServerPort) || - webServerPort <= 0 || - webServerPort > 65_535 -) { - throw new Error("WEB_SERVER_PORT doit être compris entre 1 et 65535"); +if (!Number.isInteger(webServerPort) || webServerPort <= 0) { + throw new Error("WEB_SERVER_PORT doit être un entier positif"); } const config = Object.freeze({ environnement: process.env.NODE_ENV, + location: process.env.LOCATION, + botOwner: process.env.BOT_OWNER, discordGuildId: process.env.DISCORD_GUILD_ID, shinuwaUserId: process.env.SHINUWA_USER_ID, + inviteLink: process.env.INVITE_LINK, webServerPort, webApiKey: process.env.WEB_API_KEY, applicationId: process.env.DISCORD_APPLICATION_ID, diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 435e7a1..f75b69f 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -5,7 +5,9 @@ const event = { async execute(interaction) { if (!interaction.isChatInputCommand()) return; - const command = interaction.client.commands.get(interaction.commandName)?.command; + const command = interaction.client.commands.get( + interaction.commandName + ).command; if (!command) { console.error( diff --git a/src/events/ready.js b/src/events/ready.js index d45a1df..7a0da5d 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -1,20 +1,26 @@ -import { Events } from "discord.js"; -import { showGameServers } from "../features/showGameServers.js"; +import { Events } from "discord.js"; +import { config } from "../config.js"; +import { showGameServers } from "../features/showGameServers.js"; +import { sendMessage } from "../features/sendMessage.js"; +import { isConnectedToVoiceChannel } from "../features/utilities.js"; const event = { name: Events.ClientReady, once: true, - async execute(client) { + execute(client) { console.log( `[${new Date().toLocaleString()}] Logged in as ${client.user.tag}` ); - try { - await showGameServers(client); - } catch (error) { - console.error("Unable to initialize game server status", error); - } - }, -}; + showGameServers( + client, + config.textChannelId, + config.gameServersMessageId, + config.shinuwaUserId + ); + + //sendMessage(client, 'jukebox') + }, +}; export { event }; diff --git a/src/features/permissions.js b/src/features/permissions.js deleted file mode 100644 index 268ad95..0000000 --- a/src/features/permissions.js +++ /dev/null @@ -1,13 +0,0 @@ -async function requirePermission(interaction, permission) { - if (interaction.memberPermissions?.has(permission)) { - return true; - } - - await interaction.reply({ - content: "Tu n'as pas la permission d'utiliser cette commande.", - ephemeral: true, - }); - return false; -} - -export { requirePermission }; diff --git a/src/features/sendMessage.js b/src/features/sendMessage.js new file mode 100644 index 0000000..0e977bf --- /dev/null +++ b/src/features/sendMessage.js @@ -0,0 +1,59 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + channelMention, + codeBlock, +} from "discord.js"; +import { config } from "../config.js"; + +function sendMessage(client, type) { + const channel = client.channels.cache.get(config.textChannelId); + var message = { content: "Message réservé" }; + + if (type && type === "invitation") { + message.content = inviteMessage(); + } else if (type && type === "jukebox") { + message = jukeboxMessage(); + } + + channel.send(message); +} + +function inviteMessage() { + var message = + "Pour inviter quelqu'un, il faut utiliser cette invitation temporaire !\n"; + + message = message + codeBlock(config.inviteLink); + + message = + message + + "*Pour une invitation permanente, il suffit d'attribuer le role*" + + " ***Hangaround*** " + + "*à la personne concernée.*\n\n" + + "​"; + + return message; +} + +function jukeboxMessage() { + const description = + "Yo ! 😺\n\n" + + `J'ai installé un jukebox dans ${channelMention(config.voiceChannelId)} !\n` + + "Tu peux le contrôler facilement via une petite interface ! 😽.\n\n" + + "Clique sur le bouton ci-dessous pour ouvrir le tableau de bord.\n\n"; + + const buttonRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Ouvrir le Jukebox 🎵") + .setStyle(ButtonStyle.Link) + .setURL("https://palico-bot.shinuwa.fr/jukebox/") + ); + + return { + content: description, + components: [buttonRow], + }; +} + +export { sendMessage, inviteMessage, jukeboxMessage }; diff --git a/src/features/showGameServers.js b/src/features/showGameServers.js index f037f00..43478b1 100644 --- a/src/features/showGameServers.js +++ b/src/features/showGameServers.js @@ -3,7 +3,7 @@ import { getJSONData } from "./utilities.js"; var currentStatus = []; -async function checkServersStatus(client) { +function checkServersStatus(client) { const data = getJSONData("src/static/gameservers-list.json"); const newStatus = data.servers.map((server) => server.active); @@ -11,12 +11,12 @@ async function checkServersStatus(client) { return; } - await updateMessage(client, data); + updateMessage(client, data); currentStatus = newStatus; return true; } -async function updateMessage(client, data) { +function updateMessage(client, data) { var newMessage = `Hello 😺 ! Voila les serveurs de jeux qu'on a chez nous :\n\n`; data.servers.forEach((server) => { @@ -29,33 +29,36 @@ async function updateMessage(client, data) { "```"; }); - newMessage = + newMessage = + newMessage + + "\nAh d'ailleurs ! Si le serveur est sécurisé, le mot de passe c'est surement **0117**."; + newMessage = newMessage + `\nPour mettre en ligne un serveur, faut voir avec <@${config.shinuwaUserId}> , c'est lui qui gère ca !\n`; - const channel = client.channels.cache.get(config.textChannelId); - if (!channel?.isTextBased()) { - throw new Error("Salon textuel configuré introuvable"); - } - - const message = await channel.messages.fetch(config.gameServersMessageId); - await message.edit(newMessage); - console.log( - `[${new Date().toLocaleString()}] showGameServers: Game servers message updated` - ); -} - -async function showGameServers(client) { + const channel = client.channels.cache.get(config.textChannelId); + + channel.messages + .fetch(config.gameServersMessageId) + .then((message) => { + message + .edit(newMessage) + .then( + console.log( + `[${new Date().toLocaleString()}] showGameServers: Game servers message updated` + ) + ) + .catch(console.error); + }) + .catch(console.error); +} + +function showGameServers(client) { const data = getJSONData("src/static/gameservers-list.json"); currentStatus = data.servers.map((server) => server.active); - await updateMessage(client, data); - const interval = setInterval(() => { - checkServersStatus(client).catch((error) => - console.error("Unable to update game server status", error) - ); - }, 120000); - interval.unref(); + updateMessage(client, data); + setInterval(checkServersStatus, 120000, client); } export { showGameServers }; diff --git a/src/features/utilities.js b/src/features/utilities.js index 9bd332d..50fca3f 100644 --- a/src/features/utilities.js +++ b/src/features/utilities.js @@ -1,53 +1,44 @@ import fs from "fs"; import { config } from "../config.js"; -async function changeVoiceStatus(client, status, members) { - const changeStatus = async (type, value, selectedMembers) => { - const operations = []; - switch (type) { - case "mute": - for (const member of selectedMembers.values()) { - operations.push(member.voice.setMute(value)); - } - break; - case "deaf": - for (const member of selectedMembers.values()) { - operations.push(member.voice.setDeaf(value)); - } - break; - } - await Promise.all(operations); - }; - - const voiceChannel = client.channels.cache.get(config.voiceChannelId); - if (!voiceChannel?.isVoiceBased()) { - throw new Error("Salon vocal configuré introuvable"); - } - const connectedMembers = voiceChannel.members.filter( - (member) => !member.user.bot - ); - - if (members === "all") { - members = connectedMembers; - } - - switch (status) { - case "mute": - await changeStatus("mute", true, members); - break; - case "unmute": - await changeStatus("mute", false, members); - break; - case "deaf": - await changeStatus("deaf", true, members); - break; - case "undeaf": - await changeStatus("deaf", false, members); - break; - default: - throw new Error(`Statut vocal inconnu : ${status}`); - } -} +function changeVoiceStatus(client, status, members) { + const changeStatus = (type, value, members) => { + switch (type) { + case "mute": + for (let member of members) { + member[1].voice.setMute(value); + } + break; + case "deaf": + for (let member of members) { + member[1].voice.setDeaf(value); + } + break; + } + }; + + var voiceChannel = client.channels.cache.get(config.voiceChannelId); + var connectedMembers = voiceChannel.members; + + if (members == "all") { + members = connectedMembers; + } + + switch (status) { + case "mute": + changeStatus("mute", true, members); + break; + case "unmute": + changeStatus("mute", false, members); + break; + case "deaf": + changeStatus("deaf", true, members); + break; + case "undeaf": + changeStatus("deaf", false, members); + break; + } +} function getJSONData(file) { const jsonFile = fs.readFileSync(file); @@ -60,8 +51,24 @@ function getRandomInt(max) { return Math.floor(Math.random() * max); } -export { - changeVoiceStatus, - getJSONData, - getRandomInt, -}; +function isConnectedToVoiceChannel(client, userId) { + var voiceChannel = client.channels.cache.get(config.voiceChannelId); + var connectedMembers = voiceChannel.members; + + var selectedMember = connectedMembers.filter( + (member) => member.user.id === userId + ); + + if (selectedMember.size === 1) { + return true; + } + + return false; +} + +export { + changeVoiceStatus, + getJSONData, + getRandomInt, + isConnectedToVoiceChannel, +}; diff --git a/src/media/validateAudioFile.js b/src/media/validateAudioFile.js deleted file mode 100644 index 7cbb0aa..0000000 --- a/src/media/validateAudioFile.js +++ /dev/null @@ -1,35 +0,0 @@ -import { execFile } from "node:child_process"; -import { promisify } from "node:util"; - -const execFileAsync = promisify(execFile); - -async function validateAudioFile(filePath) { - try { - const { stdout } = await execFileAsync( - "ffprobe", - [ - "-v", - "error", - "-select_streams", - "a:0", - "-show_entries", - "stream=codec_type", - "-of", - "default=noprint_wrappers=1:nokey=1", - filePath, - ], - { timeout: 15_000, maxBuffer: 1024 * 1024 } - ); - - if (stdout.trim() !== "audio") { - throw new Error("Aucune piste audio détectée"); - } - } catch (error) { - const validationError = new Error("Le fichier fourni n'est pas un audio valide"); - validationError.status = 400; - validationError.cause = error; - throw validationError; - } -} - -export { validateAudioFile }; diff --git a/src/music/queue.js b/src/music/queue.js index db7692e..1bbc880 100644 --- a/src/music/queue.js +++ b/src/music/queue.js @@ -11,7 +11,6 @@ import { } from "@discordjs/voice"; import { Events } from "discord.js"; import { config } from "../config.js"; -import { serializeTrack, serializeTracks } from "../web/serializers.js"; import { deleteTrackFile, handleTrackCompletion, @@ -30,8 +29,6 @@ class MusicQueue extends EventEmitter { this.idleDisconnectTimeout = null; this.aloneDisconnectTimeout = null; this.suspendedForAlone = false; - this.playNextPromise = null; - this.shuttingDown = false; this.audioPlayer = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play, @@ -52,18 +49,13 @@ class MusicQueue extends EventEmitter { handleTrackCompletion(this.currentTrack); } this.currentTrack = null; - if (!this.shuttingDown) { - void this.playNext(); - } + this.playNext(); }); this.audioPlayer.on("error", (error) => { console.error("Audio player encountered an error", error); - handleTrackCompletion(this.currentTrack); this.currentTrack = null; - if (!this.shuttingDown) { - void this.playNext(); - } + this.playNext(); }); this.audioPlayer.on("stateChange", (oldState, newState) => { @@ -74,27 +66,18 @@ class MusicQueue extends EventEmitter { snapshot() { return { - current: serializeTrack(this.currentTrack), - upcoming: serializeTracks(this.queue), + current: this.currentTrack, + upcoming: this.queue, status: this.playbackStatus, }; } async ensureConnection() { - if (this.connection) { - if ( - this.connection.joinConfig.channelId === config.voiceChannelId && - this.connection.state.status === VoiceConnectionStatus.Ready - ) { - return this.connection; - } - - try { - this.connection.destroy(); - } catch { - // The connection may already be destroyed. - } - this.connection = null; + if ( + this.connection && + this.connection.joinConfig.channelId === config.voiceChannelId + ) { + return this.connection; } let channel = this.client.channels.cache.get(config.voiceChannelId); @@ -121,32 +104,7 @@ class MusicQueue extends EventEmitter { this.connection = connection; connection.subscribe(this.audioPlayer); - connection.on(VoiceConnectionStatus.Disconnected, async () => { - try { - await Promise.race([ - entersState(connection, VoiceConnectionStatus.Signalling, 5_000), - entersState(connection, VoiceConnectionStatus.Connecting, 5_000), - ]); - } catch { - if (this.connection === connection) { - this.destroyConnection("voice connection lost"); - } - } - }); - - try { - await entersState(connection, VoiceConnectionStatus.Ready, 30_000); - } catch (error) { - if (this.connection === connection) { - try { - connection.destroy(); - } catch { - // The connection may have been destroyed by the voice adapter. - } - this.connection = null; - } - throw error; - } + await entersState(connection, VoiceConnectionStatus.Ready, 30_000); this.evaluateAloneStatus(); return connection; } @@ -156,24 +114,13 @@ class MusicQueue extends EventEmitter { this.cancelIdleDisconnect(); this.emit("queueUpdate", this.snapshot()); if (!this.currentTrack) { - void this.playNext(); + return this.playNext(); } return track; } - playNext() { - if (this.playNextPromise) { - return this.playNextPromise; - } - - this.playNextPromise = this.runPlayNext().finally(() => { - this.playNextPromise = null; - }); - return this.playNextPromise; - } - - async runPlayNext() { + async playNext() { if (this.suspendedForAlone) { return; } @@ -185,43 +132,32 @@ class MusicQueue extends EventEmitter { } this.cancelIdleDisconnect(); - const nextTrack = this.queue[0]; + const nextTrack = this.queue.shift(); + this.emit("queueUpdate", this.snapshot()); try { await this.ensureConnection(); } catch (error) { console.error("Unable to connect to the configured voice channel", error); + deleteTrackFile(nextTrack); this.currentTrack = null; return; } - if ( - this.shuttingDown || - this.suspendedForAlone || - this.queue[0]?.id !== nextTrack.id - ) { - return; - } - try { - this.queue.shift(); const stream = fs.createReadStream(nextTrack.path); const resource = createAudioResource(stream, { inlineVolume: true, }); - this.currentTrack = { - ...nextTrack, - startedAt: new Date().toISOString(), - }; + this.currentTrack = nextTrack; this.audioPlayer.play(resource); - this.emit("trackStart", this.currentTrack); - this.emit("queueUpdate", this.snapshot()); + this.emit("trackStart", nextTrack); } catch (error) { console.error("Unable to play track", error); deleteTrackFile(nextTrack); this.currentTrack = null; - return this.runPlayNext(); + return this.playNext(); } } @@ -258,7 +194,7 @@ class MusicQueue extends EventEmitter { } skip() { - return this.audioPlayer.stop(true); + this.audioPlayer.stop(true); } stop() { @@ -326,9 +262,6 @@ class MusicQueue extends EventEmitter { this.cancelIdleDisconnect(); this.cancelAloneDisconnect(); - const interruptedTrack = this.currentTrack; - this.currentTrack = null; - if (this.connection) { try { this.connection.destroy(); @@ -344,7 +277,7 @@ class MusicQueue extends EventEmitter { console.error("Failed to stop audio player", error); } - handleTrackCompletion(interruptedTrack); + this.currentTrack = null; this.emit("queueUpdate", this.snapshot()); console.log( `[${new Date().toLocaleString()}] MusicQueue disconnected (${reason})` @@ -409,7 +342,7 @@ class MusicQueue extends EventEmitter { this.suspendedForAlone = false; this.cancelAloneDisconnect(); if (wasSuspended && !this.currentTrack && this.queue.length > 0) { - void this.playNext(); + this.playNext(); } } } @@ -432,17 +365,6 @@ class MusicQueue extends EventEmitter { const others = channel.members.filter((member) => member.id !== clientId); return others.size === 0; } - - shutdown() { - this.shuttingDown = true; - this.cancelIdleDisconnect(); - this.cancelAloneDisconnect(); - this.client.off(Events.VoiceStateUpdate, this.voiceStateListener); - - this.queue.forEach((track) => deleteTrackFile(track)); - this.queue = []; - this.destroyConnection("shutdown"); - } } export { MusicQueue }; diff --git a/src/static/gameservers-list.example.json b/src/static/gameservers-list.example.json deleted file mode 100644 index a251775..0000000 --- a/src/static/gameservers-list.example.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "servers": [ - { - "game": "Nom du jeu", - "address": "games.example.com:25565", - "port": 25565, - "active": false - } - ] -} diff --git a/src/web/middlewares/auth.js b/src/web/middlewares/auth.js index 46bc954..11ca531 100644 --- a/src/web/middlewares/auth.js +++ b/src/web/middlewares/auth.js @@ -1,9 +1,4 @@ import { config } from "../../config.js"; -import { createHash, timingSafeEqual } from "node:crypto"; - -function tokenDigest(value) { - return createHash("sha256").update(value).digest(); -} function authMiddleware(req, res, next) { if (!config.webApiKey) { @@ -11,10 +6,9 @@ function authMiddleware(req, res, next) { } const header = req.headers.authorization || ""; - const match = /^Bearer ([^\s]+)$/.exec(header); - const token = match?.[1] || ""; + const token = header.replace("Bearer ", ""); - if (timingSafeEqual(tokenDigest(token), tokenDigest(config.webApiKey))) { + if (token === config.webApiKey) { return next(); } diff --git a/src/web/routes/library.js b/src/web/routes/library.js index 3087db3..07c79f1 100644 --- a/src/web/routes/library.js +++ b/src/web/routes/library.js @@ -5,13 +5,12 @@ import { findPermanentTrack, getPermanentLibrary, } from "../storage/storageManager.js"; -import { serializeTrack, serializeTracks } from "../serializers.js"; function libraryRouter({ queue }) { const router = Router(); router.get("/", (req, res) => { - res.json({ tracks: serializeTracks(getPermanentLibrary()) }); + res.json({ tracks: getPermanentLibrary() }); }); router.delete("/:libraryId", (req, res) => { @@ -22,7 +21,7 @@ function libraryRouter({ queue }) { } queue.removeByLibraryId(track.id); - res.json({ removed: serializeTrack(track) }); + res.json({ removed: track }); }); router.post("/:libraryId/enqueue", (req, res) => { @@ -36,7 +35,7 @@ function libraryRouter({ queue }) { req.body?.requestedBy ); queue.enqueue(queueTrack); - res.status(201).json({ track: serializeTrack(queueTrack) }); + res.status(201).json({ track: queueTrack }); }); return router; diff --git a/src/web/routes/queue.js b/src/web/routes/queue.js index ef7c7a4..a35a58a 100644 --- a/src/web/routes/queue.js +++ b/src/web/routes/queue.js @@ -1,6 +1,5 @@ import { Router } from "express"; import { createQueueTrackFromLibrary, findPermanentTrack } from "../storage/storageManager.js"; -import { serializeTrack } from "../serializers.js"; function queueRouter({ queue }) { const router = Router(); @@ -10,7 +9,7 @@ function queueRouter({ queue }) { }); router.post("/", (req, res) => { - const { libraryTrackId, requestedBy } = req.body || {}; + const { libraryTrackId, requestedBy } = req.body; if (!libraryTrackId) { return res.status(400).json({ error: "libraryTrackId manquant" }); @@ -24,7 +23,7 @@ function queueRouter({ queue }) { const track = createQueueTrackFromLibrary(libraryTrack, requestedBy); queue.enqueue(track); - res.status(201).json({ track: serializeTrack(track) }); + res.status(201).json({ track }); }); router.delete("/:trackId", async (req, res) => { @@ -33,13 +32,13 @@ function queueRouter({ queue }) { return res.status(404).json({ error: "Piste introuvable" }); } - res.json({ removed: serializeTrack(removed) }); + res.json({ removed }); }); router.patch("/:trackId/move", (req, res) => { const position = Number(req.body?.position); - if (!Number.isInteger(position) || position < 0) { - return res.status(400).json({ error: "position doit être un entier positif" }); + if (Number.isNaN(position)) { + return res.status(400).json({ error: "position doit être un nombre" }); } const snapshot = queue.move(req.params.trackId, position); diff --git a/src/web/routes/uploads.js b/src/web/routes/uploads.js index 1d69ef2..2daf273 100644 --- a/src/web/routes/uploads.js +++ b/src/web/routes/uploads.js @@ -1,41 +1,11 @@ import { Router } from "express"; import multer from "multer"; -import { validateAudioFile } from "../../media/validateAudioFile.js"; -import { serializeTrack } from "../serializers.js"; -import { - TEMP_DIR, - discardUploadedFile, - registerUploadedFile, -} from "../storage/storageManager.js"; - -const ALLOWED_AUDIO_TYPES = new Set([ - "audio/aac", - "audio/flac", - "audio/mp4", - "audio/mpeg", - "audio/ogg", - "audio/wav", - "audio/webm", - "audio/x-flac", - "audio/x-m4a", - "audio/x-wav", -]); +import { TEMP_DIR, registerUploadedFile } from "../storage/storageManager.js"; const upload = multer({ dest: TEMP_DIR, limits: { fileSize: 150 * 1024 * 1024, - fields: 4, - fieldSize: 512, - }, - fileFilter: (_req, file, callback) => { - if (file.originalname.length > 255) { - const error = new Error("Le nom du fichier est trop long"); - error.status = 400; - callback(error); - return; - } - callback(null, ALLOWED_AUDIO_TYPES.has(file.mimetype)); }, }); @@ -49,41 +19,17 @@ function uploadsRouter({ queue }) { const permanent = req.body?.permanent === "true"; const enqueue = req.body?.enqueue !== "false"; - const uploader = String(req.body?.uploader || "web-ui") - .replace(/[\u0000-\u001f]/g, "") - .trim() - .slice(0, 80); - const title = String(req.body?.notes || req.file.originalname) - .replace(/[\u0000-\u001f]/g, "") - .trim() - .slice(0, 200); + const uploader = req.body?.uploader || "web-ui"; try { - if (!permanent && !enqueue) { - const error = new Error( - "Un fichier temporaire doit être ajouté à la file d'attente" - ); - error.status = 400; - throw error; - } - - await validateAudioFile(req.file.path); - const track = registerUploadedFile(req.file, { - permanent, - uploader, - title: title || req.file.originalname, - }); + const track = registerUploadedFile(req.file, { permanent, uploader }); if (enqueue) { queue.enqueue(track); } - return res.status(201).json({ - track: serializeTrack(track), - enqueued: enqueue, - }); + return res.status(201).json({ track, enqueued: enqueue }); } catch (error) { - discardUploadedFile(req.file); return next(error); } }); diff --git a/src/web/serializers.js b/src/web/serializers.js deleted file mode 100644 index c861b8a..0000000 --- a/src/web/serializers.js +++ /dev/null @@ -1,14 +0,0 @@ -function serializeTrack(track) { - if (!track) { - return null; - } - - const { path: _path, storedName: _storedName, ...publicTrack } = track; - return publicTrack; -} - -function serializeTracks(tracks = []) { - return tracks.map(serializeTrack); -} - -export { serializeTrack, serializeTracks }; diff --git a/src/web/server.js b/src/web/server.js index cae15c7..b0868c2 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -1,7 +1,4 @@ import express from "express"; -import helmet from "helmet"; -import { rateLimit } from "express-rate-limit"; -import multer from "multer"; import path from "path"; import { config } from "../config.js"; import { authMiddleware } from "./middlewares/auth.js"; @@ -9,56 +6,19 @@ import { libraryRouter } from "./routes/library.js"; import { playerRouter } from "./routes/player.js"; import { queueRouter } from "./routes/queue.js"; import { uploadsRouter } from "./routes/uploads.js"; -import { - cleanupTemporaryFiles, - ensureStorageLayout, -} from "./storage/storageManager.js"; +import { ensureStorageLayout } from "./storage/storageManager.js"; const PUBLIC_DIR = path.join(process.cwd(), "public"); function startWebServer(dependencies) { ensureStorageLayout(); - cleanupTemporaryFiles(); const app = express(); - app.disable("x-powered-by"); - app.use(helmet()); - app.use(express.json({ limit: "32kb" })); - - app.get("/healthz", (_req, res) => { - res.json({ status: "ok" }); - }); + app.use(express.json()); const apiRouter = express.Router(); - apiRouter.use( - rateLimit({ - windowMs: 60_000, - limit: 120, - standardHeaders: "draft-8", - legacyHeaders: false, - }) - ); - apiRouter.use( - rateLimit({ - windowMs: 15 * 60_000, - limit: 20, - skipSuccessfulRequests: true, - standardHeaders: "draft-8", - legacyHeaders: false, - message: { error: "Trop de tentatives d'authentification" }, - }) - ); apiRouter.use(authMiddleware); - apiRouter.use( - "/uploads", - rateLimit({ - windowMs: 15 * 60_000, - limit: 10, - standardHeaders: "draft-8", - legacyHeaders: false, - }), - uploadsRouter(dependencies) - ); + apiRouter.use("/uploads", uploadsRouter(dependencies)); apiRouter.use("/queue", queueRouter(dependencies)); apiRouter.use("/library", libraryRouter(dependencies)); apiRouter.use("/player", playerRouter(dependencies)); @@ -69,28 +29,15 @@ function startWebServer(dependencies) { ); app.use(express.static(PUBLIC_DIR)); - app.use((error, _req, res, _next) => { - let status = Number(error?.status) || 500; - if (error instanceof multer.MulterError) { - status = error.code === "LIMIT_FILE_SIZE" ? 413 : 400; - } - if (status >= 500) { - console.error("Unhandled HTTP error", error); - } - res.status(status).json({ - error: status >= 500 ? "Erreur interne du serveur" : error.message, - }); - }); - const port = Number(process.env.PORT || config.webServerPort || 3000); - const server = app.listen(port, () => { + app.listen(port, () => { console.log( `[${new Date().toLocaleString()}] Web control server listening on port ${port}` ); }); - return { app, server }; + return app; } export { startWebServer }; diff --git a/src/web/storage/storageManager.js b/src/web/storage/storageManager.js index a6c7b18..079d594 100644 --- a/src/web/storage/storageManager.js +++ b/src/web/storage/storageManager.js @@ -1,15 +1,11 @@ import fs from "fs"; import path from "path"; -import { randomUUID } from "node:crypto"; +import { v4 as uuid } from "uuid"; const STORAGE_ROOT = path.join(process.cwd(), "storage"); const PERMANENT_DIR = path.join(STORAGE_ROOT, "permanent"); const TEMP_DIR = path.join(STORAGE_ROOT, "temp"); -const RUNTIME_DIR = path.join(STORAGE_ROOT, "runtime"); -const LIBRARY_FILE = path.join(RUNTIME_DIR, "library.json"); -const LEGACY_LIBRARY_FILE = path.join(STORAGE_ROOT, "library.json"); -const TEMP_FILE_MAX_AGE = 24 * 60 * 60 * 1000; -const MAX_LIBRARY_SIZE = 2 * 1024 * 1024 * 1024; +const LIBRARY_FILE = path.join(STORAGE_ROOT, "library.json"); function ensureStorageLayout() { if (!fs.existsSync(STORAGE_ROOT)) { @@ -24,71 +20,26 @@ function ensureStorageLayout() { fs.mkdirSync(TEMP_DIR, { recursive: true }); } - if (!fs.existsSync(RUNTIME_DIR)) { - fs.mkdirSync(RUNTIME_DIR, { recursive: true }); - } - if (!fs.existsSync(LIBRARY_FILE)) { - let initialEntries = []; - if (fs.existsSync(LEGACY_LIBRARY_FILE)) { - try { - initialEntries = JSON.parse( - fs.readFileSync(LEGACY_LIBRARY_FILE, { encoding: "utf8" }) - ); - } catch (error) { - throw new Error("Impossible de migrer l'ancienne bibliothèque", { - cause: error, - }); - } - } - const portableEntries = Array.isArray(initialEntries) - ? initialEntries.map(({ path: _path, ...entry }) => entry) - : []; - fs.writeFileSync(LIBRARY_FILE, JSON.stringify(portableEntries, null, 2)); - if (portableEntries.length > 0) { - console.log( - `[${new Date().toLocaleString()}] Migrated ${portableEntries.length} library entries` - ); - } + fs.writeFileSync(LIBRARY_FILE, JSON.stringify([])); } } function readLibrary() { try { const raw = fs.readFileSync(LIBRARY_FILE, { encoding: "utf8" }); - const entries = JSON.parse(raw); - if (!Array.isArray(entries)) { - throw new TypeError("library.json doit contenir un tableau"); - } - - return entries - .filter( - (entry) => - entry && - typeof entry.storedName === "string" && - path.basename(entry.storedName) === entry.storedName - ) - .map((entry) => ({ - ...entry, - path: path.join(PERMANENT_DIR, entry.storedName), - })); + return JSON.parse(raw); } catch (error) { console.error("Failed to read library file", error); - throw new Error("Impossible de lire la bibliothèque", { cause: error }); + return []; } } function writeLibrary(entries) { - const persistedEntries = entries.map(({ path: _path, ...entry }) => entry); - const temporaryFile = `${LIBRARY_FILE}.${process.pid}.tmp`; - fs.writeFileSync(temporaryFile, JSON.stringify(persistedEntries, null, 2)); - fs.renameSync(temporaryFile, LIBRARY_FILE); + fs.writeFileSync(LIBRARY_FILE, JSON.stringify(entries, null, 2)); } -function registerUploadedFile( - file, - { permanent, uploader = "unknown", title = file.originalname } -) { +function registerUploadedFile(file, { permanent, uploader = "unknown" }) { ensureStorageLayout(); const createdAt = new Date().toISOString(); let finalPath = file.path; @@ -96,26 +47,13 @@ function registerUploadedFile( let permanentEntry = null; if (permanent) { - const library = readLibrary(); - const librarySize = library.reduce( - (total, entry) => total + (Number(entry.size) || 0), - 0 - ); - if (librarySize + file.size > MAX_LIBRARY_SIZE) { - throw new Error("La bibliothèque permanente a atteint sa limite de 2 Go"); - } - - const rawExtension = path.extname(file.originalname).toLowerCase(); - const extension = /^\.[a-z0-9]{1,10}$/.test(rawExtension) - ? rawExtension - : ""; - storedName = `${randomUUID()}${extension.toLowerCase()}`; + const extension = path.extname(file.originalname) || ""; + storedName = `${uuid()}${extension}`; finalPath = path.join(PERMANENT_DIR, storedName); fs.renameSync(file.path, finalPath); permanentEntry = { - id: randomUUID(), + id: uuid(), originalName: file.originalname, - title, storedName, path: finalPath, size: file.size, @@ -123,13 +61,14 @@ function registerUploadedFile( uploader, createdAt, }; + const library = readLibrary(); library.push(permanentEntry); writeLibrary(library); } return { - id: randomUUID(), - title, + id: uuid(), + title: file.originalname, originalName: file.originalname, path: finalPath, storedName, @@ -157,8 +96,8 @@ function createQueueTrackFromLibrary(libraryTrack, requestedBy = "library") { } return { - id: randomUUID(), - title: libraryTrack.title || libraryTrack.originalName, + id: uuid(), + title: libraryTrack.originalName, originalName: libraryTrack.originalName, path: libraryTrack.path, storedName: libraryTrack.storedName, @@ -171,26 +110,6 @@ function createQueueTrackFromLibrary(libraryTrack, requestedBy = "library") { }; } -function cleanupTemporaryFiles(maxAge = TEMP_FILE_MAX_AGE) { - ensureStorageLayout(); - const cutoff = Date.now() - maxAge; - - for (const entry of fs.readdirSync(TEMP_DIR, { withFileTypes: true })) { - if (!entry.isFile() || entry.name === ".gitkeep") { - continue; - } - - const filePath = path.join(TEMP_DIR, entry.name); - try { - if (fs.statSync(filePath).mtimeMs < cutoff) { - fs.unlinkSync(filePath); - } - } catch (error) { - console.error(`Failed to clean temporary file ${filePath}`, error); - } - } -} - function deleteFileSafe(filePath) { if (!filePath) { return; @@ -203,10 +122,6 @@ function deleteFileSafe(filePath) { } } -function discardUploadedFile(file) { - deleteFileSafe(file?.path); -} - function deletePermanentTrack(id) { const library = getPermanentLibrary(); const index = library.findIndex((entry) => entry.id === id); @@ -245,8 +160,6 @@ export { getPermanentLibrary, findPermanentTrack, createQueueTrackFromLibrary, - cleanupTemporaryFiles, - discardUploadedFile, deletePermanentTrack, handleTrackCompletion, deleteTrackFile, diff --git a/test/auth.test.js b/test/auth.test.js deleted file mode 100644 index c9b6722..0000000 --- a/test/auth.test.js +++ /dev/null @@ -1,49 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; - -Object.assign(process.env, { - NODE_ENV: "test", - DISCORD_GUILD_ID: "1", - SHINUWA_USER_ID: "1", - WEB_SERVER_PORT: "3000", - WEB_API_KEY: "test-key", - DISCORD_APPLICATION_ID: "1", - DISCORD_TOKEN: "test-token", - VOICE_CHANNEL_ID: "1", - TEXT_CHANNEL_ID: "1", - GAME_SERVERS_MESSAGE_ID: "1", -}); - -const { authMiddleware } = await import("../src/web/middlewares/auth.js"); - -function runAuth(authorization) { - let nextCalled = false; - let statusCode; - let payload; - const request = { headers: { authorization } }; - const response = { - status(code) { - statusCode = code; - return this; - }, - json(value) { - payload = value; - return this; - }, - }; - - authMiddleware(request, response, () => { - nextCalled = true; - }); - return { nextCalled, statusCode, payload }; -} - -test("API authentication accepts only an exact Bearer token", () => { - assert.equal(runAuth("Bearer test-key").nextCalled, true); - assert.deepEqual(runAuth("test-key"), { - nextCalled: false, - statusCode: 401, - payload: { error: "Unauthorized" }, - }); - assert.equal(runAuth("Bearer wrong-key").statusCode, 401); -}); diff --git a/test/commands.test.js b/test/commands.test.js deleted file mode 100644 index 92a0184..0000000 --- a/test/commands.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs"; - -const ENVIRONMENT = { - NODE_ENV: "test", - DISCORD_GUILD_ID: "1", - SHINUWA_USER_ID: "1", - WEB_SERVER_PORT: "3000", - WEB_API_KEY: "test-key", - DISCORD_APPLICATION_ID: "1", - DISCORD_TOKEN: "test-token", - VOICE_CHANNEL_ID: "1", - TEXT_CHANNEL_ID: "1", - GAME_SERVERS_MESSAGE_ID: "1", -}; -Object.assign(process.env, ENVIRONMENT); - -async function loadCommands() { - const files = fs - .readdirSync("src/commands", { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .flatMap((directory) => - fs - .readdirSync(`src/commands/${directory.name}`) - .filter((file) => file.endsWith(".js")) - .map((file) => `../src/commands/${directory.name}/${file}`) - ); - return Promise.all(files.map((file) => import(file))); -} - -test("all Discord commands expose valid command data", async () => { - const modules = await loadCommands(); - assert.equal(modules.length, 9); - for (const module of modules) { - assert.doesNotThrow(() => module.command.data.toJSON()); - assert.equal(typeof module.command.execute, "function"); - } -}); - -test("administrative commands require Discord permissions", async () => { - const modules = await loadCommands(); - const protectedCommands = new Set([ - "deletemsgs", - "muteall", - "unmuteall", - "deafall", - "undeafall", - ]); - - for (const module of modules) { - const data = module.command.data.toJSON(); - if (protectedCommands.has(data.name)) { - assert.notEqual(data.default_member_permissions, undefined, data.name); - assert.notEqual(data.default_member_permissions, null, data.name); - } - } -}); diff --git a/test/musicQueue.test.js b/test/musicQueue.test.js deleted file mode 100644 index 2b56a77..0000000 --- a/test/musicQueue.test.js +++ /dev/null @@ -1,52 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; - -Object.assign(process.env, { - NODE_ENV: "test", - DISCORD_GUILD_ID: "1", - SHINUWA_USER_ID: "1", - WEB_SERVER_PORT: "3000", - WEB_API_KEY: "test-key", - DISCORD_APPLICATION_ID: "1", - DISCORD_TOKEN: "test-token", - VOICE_CHANNEL_ID: "1", - TEXT_CHANNEL_ID: "1", - GAME_SERVERS_MESSAGE_ID: "1", -}); - -const { MusicQueue } = await import("../src/music/queue.js"); - -test("playNext shares one in-flight operation", async () => { - const queue = Object.create(MusicQueue.prototype); - queue.playNextPromise = null; - let calls = 0; - let resolveOperation; - queue.runPlayNext = () => { - calls += 1; - return new Promise((resolve) => { - resolveOperation = resolve; - }); - }; - - const first = queue.playNext(); - const second = queue.playNext(); - assert.equal(first, second); - assert.equal(calls, 1); - - resolveOperation(); - await first; - assert.equal(queue.playNextPromise, null); -}); - -test("queue snapshots do not expose filesystem paths", () => { - const queue = Object.create(MusicQueue.prototype); - queue.currentTrack = { id: "current", path: "/private/current.mp3" }; - queue.queue = [{ id: "next", path: "/private/next.mp3" }]; - queue.playbackStatus = "playing"; - - assert.deepEqual(queue.snapshot(), { - current: { id: "current" }, - upcoming: [{ id: "next" }], - status: "playing", - }); -}); diff --git a/test/serializers.test.js b/test/serializers.test.js deleted file mode 100644 index 595d8c2..0000000 --- a/test/serializers.test.js +++ /dev/null @@ -1,21 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { serializeTrack, serializeTracks } from "../src/web/serializers.js"; - -test("serializeTrack removes internal filesystem fields", () => { - const result = serializeTrack({ - id: "track-1", - title: "Test", - path: "/private/storage/test.mp3", - storedName: "test.mp3", - }); - - assert.deepEqual(result, { id: "track-1", title: "Test" }); -}); - -test("serializeTracks handles an array and null tracks", () => { - assert.deepEqual(serializeTracks([{ id: "1", path: "/tmp/1" }]), [ - { id: "1" }, - ]); - assert.equal(serializeTrack(null), null); -}); diff --git a/test/storageManager.test.js b/test/storageManager.test.js deleted file mode 100644 index 15bb48a..0000000 --- a/test/storageManager.test.js +++ /dev/null @@ -1,83 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; - -test("the library persists portable entries and restores absolute runtime paths", async () => { - const originalDirectory = process.cwd(); - const temporaryDirectory = fs.mkdtempSync( - path.join(os.tmpdir(), "palico-storage-") - ); - - try { - process.chdir(temporaryDirectory); - const storage = await import( - `../src/web/storage/storageManager.js?test=${Date.now()}` - ); - storage.ensureStorageLayout(); - - const uploadedPath = path.join(storage.TEMP_DIR, "upload"); - fs.writeFileSync(uploadedPath, "fake audio content"); - const track = storage.registerUploadedFile( - { - path: uploadedPath, - originalname: "sample.mp3", - size: 18, - mimetype: "audio/mpeg", - }, - { permanent: true, uploader: "test", title: "Sample" } - ); - - const persisted = JSON.parse( - fs.readFileSync( - path.join(temporaryDirectory, "storage/runtime/library.json"), - "utf8" - ) - ); - assert.equal(persisted.length, 1); - assert.equal("path" in persisted[0], false); - - const restored = storage.findPermanentTrack(track.libraryTrackId); - assert.equal(path.dirname(restored.path), storage.PERMANENT_DIR); - assert.equal(fs.existsSync(restored.path), true); - } finally { - process.chdir(originalDirectory); - fs.rmSync(temporaryDirectory, { recursive: true, force: true }); - } -}); - -test("legacy library entries migrate without their absolute paths", async () => { - const originalDirectory = process.cwd(); - const temporaryDirectory = fs.mkdtempSync( - path.join(os.tmpdir(), "palico-migration-") - ); - - try { - process.chdir(temporaryDirectory); - fs.mkdirSync("storage", { recursive: true }); - fs.writeFileSync( - "storage/library.json", - JSON.stringify([ - { - id: "legacy", - storedName: "legacy.mp3", - path: "/old/server/storage/permanent/legacy.mp3", - }, - ]) - ); - - const storage = await import( - `../src/web/storage/storageManager.js?migration=${Date.now()}` - ); - storage.ensureStorageLayout(); - - const migrated = JSON.parse( - fs.readFileSync("storage/runtime/library.json", "utf8") - ); - assert.deepEqual(migrated, [{ id: "legacy", storedName: "legacy.mp3" }]); - } finally { - process.chdir(originalDirectory); - fs.rmSync(temporaryDirectory, { recursive: true, force: true }); - } -});