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 !
@@ -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 });
- }
-});