Harden bot, storage and web API
This commit is contained in:
parent
e974bc6660
commit
20624a5c9c
43 changed files with 1265 additions and 1012 deletions
|
|
@ -1,9 +1,6 @@
|
||||||
NODE_ENV=test
|
NODE_ENV=test
|
||||||
LOCATION=
|
|
||||||
BOT_OWNER=
|
|
||||||
DISCORD_GUILD_ID=
|
DISCORD_GUILD_ID=
|
||||||
SHINUWA_USER_ID=
|
SHINUWA_USER_ID=
|
||||||
INVITE_LINK=
|
|
||||||
WEB_SERVER_PORT=3000
|
WEB_SERVER_PORT=3000
|
||||||
WEB_API_KEY=
|
WEB_API_KEY=
|
||||||
DISCORD_APPLICATION_ID=
|
DISCORD_APPLICATION_ID=
|
||||||
|
|
|
||||||
17
.gitignore
vendored
17
.gitignore
vendored
|
|
@ -1,14 +1,21 @@
|
||||||
#node
|
# Node.js
|
||||||
node_modules/
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Environment files, including local variants and backups
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Local configuration and test data
|
# Local configuration and test data
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
src/static/gameservers-list.json
|
src/static/gameservers-list.json
|
||||||
|
|
||||||
#storage
|
# Runtime storage
|
||||||
|
storage/runtime/
|
||||||
storage/permanent/*
|
storage/permanent/*
|
||||||
!storage/permanent/.gitkeep
|
!storage/permanent/.gitkeep
|
||||||
storage/temp/*
|
storage/temp/*
|
||||||
!storage/temp/.gitkeep
|
!storage/temp/.gitkeep
|
||||||
|
|
||||||
|
# Operating system files
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
24
|
||||||
43
README.md
43
README.md
|
|
@ -9,6 +9,12 @@ fichier `.env` avec les valeurs appropriées.
|
||||||
Le fichier `.env.example` documente les variables attendues sans contenir de
|
Le fichier `.env.example` documente les variables attendues sans contenir de
|
||||||
secret. Les fichiers réels doivent rester hors du dépôt.
|
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
|
```bash
|
||||||
# Lancer le bot
|
# Lancer le bot
|
||||||
npm start
|
npm start
|
||||||
|
|
@ -19,3 +25,40 @@ npm run deploy
|
||||||
|
|
||||||
Node.js 22.12 ou supérieur est nécessaire pour la prise en charge du protocole
|
Node.js 22.12 ou supérieur est nécessaire pour la prise en charge du protocole
|
||||||
vocal DAVE de Discord.
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,6 @@ const rest = new REST().setToken(config.token);
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
33
index.js
33
index.js
|
|
@ -56,9 +56,36 @@ for (const file of eventFiles) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.login(config.token);
|
|
||||||
|
|
||||||
const musicQueue = new MusicQueue(client);
|
const musicQueue = new MusicQueue(client);
|
||||||
startWebServer({ queue: musicQueue });
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
export { client, musicQueue };
|
export { client, musicQueue };
|
||||||
|
|
|
||||||
485
package-lock.json
generated
485
package-lock.json
generated
|
|
@ -10,27 +10,27 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.19.2",
|
"@discordjs/voice": "^0.19.2",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.26.4",
|
||||||
"express": "^5.1.0",
|
"express": "^5.2.1",
|
||||||
"libsodium-wrappers": "^0.7.15",
|
"express-rate-limit": "^8.5.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"helmet": "^8.2.0",
|
||||||
"opusscript": "^0.0.8",
|
"multer": "^2.2.0",
|
||||||
"uuid": "^11.0.3"
|
"opusscript": "^0.0.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@discordjs/builders": {
|
"node_modules/@discordjs/builders": {
|
||||||
"version": "1.13.0",
|
"version": "1.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
|
||||||
"integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==",
|
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/formatters": "^0.6.1",
|
"@discordjs/formatters": "^0.6.2",
|
||||||
"@discordjs/util": "^1.1.1",
|
"@discordjs/util": "^1.2.0",
|
||||||
"@sapphire/shapeshift": "^4.0.0",
|
"@sapphire/shapeshift": "^4.0.0",
|
||||||
"discord-api-types": "^0.38.31",
|
"discord-api-types": "^0.38.40",
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"ts-mixer": "^6.0.4",
|
"ts-mixer": "^6.0.4",
|
||||||
"tslib": "^2.6.3"
|
"tslib": "^2.6.3"
|
||||||
|
|
@ -52,12 +52,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@discordjs/formatters": {
|
"node_modules/@discordjs/formatters": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||||
"integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==",
|
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"discord-api-types": "^0.38.1"
|
"discord-api-types": "^0.38.33"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.11.0"
|
"node": ">=16.11.0"
|
||||||
|
|
@ -67,20 +67,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@discordjs/rest": {
|
"node_modules/@discordjs/rest": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
|
||||||
"integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==",
|
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/collection": "^2.1.1",
|
"@discordjs/collection": "^2.1.1",
|
||||||
"@discordjs/util": "^1.1.1",
|
"@discordjs/util": "^1.2.0",
|
||||||
"@sapphire/async-queue": "^1.5.3",
|
"@sapphire/async-queue": "^1.5.3",
|
||||||
"@sapphire/snowflake": "^3.5.3",
|
"@sapphire/snowflake": "^3.5.5",
|
||||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||||
"discord-api-types": "^0.38.16",
|
"discord-api-types": "^0.38.40",
|
||||||
"magic-bytes.js": "^1.10.0",
|
"magic-bytes.js": "^1.13.0",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"undici": "6.21.3"
|
"undici": "6.24.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -101,11 +101,24 @@
|
||||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
"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": {
|
"node_modules/@discordjs/util": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||||
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
|
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"discord-api-types": "^0.38.33"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
|
|
@ -575,23 +588,40 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||||
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
|
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^2.0.0",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.3",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.1",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.7.2",
|
||||||
"on-finished": "^2.4.1",
|
"on-finished": "^2.4.1",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.15.2",
|
||||||
"raw-body": "^3.0.0",
|
"raw-body": "^3.0.2",
|
||||||
"type-is": "^2.0.0"
|
"type-is": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"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": {
|
"node_modules/buffer-from": {
|
||||||
|
|
@ -650,50 +680,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/concat-stream": {
|
"node_modules/concat-stream": {
|
||||||
"version": "1.6.2",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
"engines": [
|
"engines": [
|
||||||
"node >= 0.8"
|
"node >= 6.0"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"readable-stream": "^2.2.2",
|
"readable-stream": "^3.0.2",
|
||||||
"typedarray": "^0.0.6"
|
"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": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
|
||||||
|
|
@ -733,12 +733,6 @@
|
||||||
"node": ">=6.6.0"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|
@ -775,24 +769,24 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/discord.js": {
|
"node_modules/discord.js": {
|
||||||
"version": "14.24.2",
|
"version": "14.26.4",
|
||||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz",
|
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.4.tgz",
|
||||||
"integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==",
|
"integrity": "sha512-4oBp8tc6Kf8IDBwAHhbsMaAqx1b5fob9SNasZT7V6yyyUydoO5i5fGuX7TmvRtR+q/WgKRnRViRoAWnG7fNyvA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/builders": "^1.13.0",
|
"@discordjs/builders": "^1.14.1",
|
||||||
"@discordjs/collection": "1.5.3",
|
"@discordjs/collection": "1.5.3",
|
||||||
"@discordjs/formatters": "^0.6.1",
|
"@discordjs/formatters": "^0.6.2",
|
||||||
"@discordjs/rest": "^2.6.0",
|
"@discordjs/rest": "^2.6.1",
|
||||||
"@discordjs/util": "^1.1.1",
|
"@discordjs/util": "^1.2.0",
|
||||||
"@discordjs/ws": "^1.2.3",
|
"@discordjs/ws": "^1.2.3",
|
||||||
"@sapphire/snowflake": "3.5.3",
|
"@sapphire/snowflake": "3.5.3",
|
||||||
"discord-api-types": "^0.38.31",
|
"discord-api-types": "^0.38.40",
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
"lodash.snakecase": "4.1.1",
|
"lodash.snakecase": "4.1.1",
|
||||||
"magic-bytes.js": "^1.10.0",
|
"magic-bytes.js": "^1.13.0",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.3",
|
||||||
"undici": "6.21.3"
|
"undici": "6.24.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -849,9 +843,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-object-atoms": {
|
"node_modules/es-object-atoms": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
|
|
@ -876,18 +870,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "5.1.0",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "^2.0.0",
|
"accepts": "^2.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.1",
|
||||||
"content-disposition": "^1.0.0",
|
"content-disposition": "^1.0.0",
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"cookie": "^0.7.1",
|
"cookie": "^0.7.1",
|
||||||
"cookie-signature": "^1.2.1",
|
"cookie-signature": "^1.2.1",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
"encodeurl": "^2.0.0",
|
"encodeurl": "^2.0.0",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"etag": "^1.8.1",
|
"etag": "^1.8.1",
|
||||||
|
|
@ -917,6 +912,24 @@
|
||||||
"url": "https://opencollective.com/express"
|
"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": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
|
@ -1029,9 +1042,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hasown": {
|
"node_modules/hasown": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|
@ -1040,41 +1053,52 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/helmet": {
|
||||||
"version": "2.0.0",
|
"version": "8.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz",
|
||||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
"integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"depd": "2.0.0",
|
|
||||||
"inherits": "2.0.4",
|
|
||||||
"setprototypeof": "1.2.0",
|
|
||||||
"statuses": "2.0.1",
|
|
||||||
"toidentifier": "1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/EvanHahn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-errors/node_modules/statuses": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.6.3",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
|
|
@ -1083,6 +1107,15 @@
|
||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|
@ -1098,31 +1131,10 @@
|
||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.snakecase": {
|
"node_modules/lodash.snakecase": {
|
||||||
|
|
@ -1132,9 +1144,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/magic-bytes.js": {
|
"node_modules/magic-bytes.js": {
|
||||||
"version": "1.12.1",
|
"version": "1.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||||
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
|
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
|
|
@ -1188,15 +1200,6 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
|
@ -1204,22 +1207,22 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/multer": {
|
"node_modules/multer": {
|
||||||
"version": "1.4.5-lts.2",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.2.0.tgz",
|
||||||
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
|
"integrity": "sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==",
|
||||||
"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",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"append-field": "^1.0.0",
|
"append-field": "^1.0.0",
|
||||||
"busboy": "^1.0.0",
|
"busboy": "^1.6.0",
|
||||||
"concat-stream": "^1.5.2",
|
"concat-stream": "^2.0.0",
|
||||||
"mkdirp": "^0.5.4",
|
"type-is": "^1.6.18"
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"type-is": "^1.6.4",
|
|
||||||
"xtend": "^4.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6.0.0"
|
"node": ">= 10.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/multer/node_modules/media-typer": {
|
"node_modules/multer/node_modules/media-typer": {
|
||||||
|
|
@ -1252,18 +1255,6 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/multer/node_modules/type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
|
@ -1286,15 +1277,6 @@
|
||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
|
@ -1344,9 +1326,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "8.3.0",
|
"version": "8.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
|
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -1379,12 +1361,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
|
@ -1399,12 +1375,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.1.0"
|
"es-define-property": "^1.0.1",
|
||||||
|
"side-channel": "^1.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
|
|
@ -1423,34 +1400,32 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
"integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "~3.1.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "~2.0.1",
|
||||||
"iconv-lite": "0.7.0",
|
"iconv-lite": "~0.7.0",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "~1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/raw-body/node_modules/iconv-lite": {
|
"node_modules/readable-stream": {
|
||||||
"version": "0.7.0",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
"integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">= 6"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/router": {
|
"node_modules/router": {
|
||||||
|
|
@ -1539,14 +1514,14 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"object-inspect": "^1.13.3",
|
"object-inspect": "^1.13.4",
|
||||||
"side-channel-list": "^1.0.0",
|
"side-channel-list": "^1.0.1",
|
||||||
"side-channel-map": "^1.0.1",
|
"side-channel-map": "^1.0.1",
|
||||||
"side-channel-weakmap": "^1.0.2"
|
"side-channel-weakmap": "^1.0.2"
|
||||||
},
|
},
|
||||||
|
|
@ -1558,13 +1533,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/side-channel-list": {
|
"node_modules/side-channel-list": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"object-inspect": "^1.13.3"
|
"object-inspect": "^1.13.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -1627,6 +1602,15 @@
|
||||||
"node": ">=10.0.0"
|
"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": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -1649,17 +1633,34 @@
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||||
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
|
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^2.0.0",
|
||||||
"media-typer": "^1.1.0",
|
"media-typer": "^1.1.0",
|
||||||
"mime-types": "^3.0.0"
|
"mime-types": "^3.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"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_modules/typedarray": {
|
"node_modules/typedarray": {
|
||||||
|
|
@ -1669,9 +1670,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.21.3",
|
"version": "6.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
||||||
"integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==",
|
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|
@ -1698,19 +1699,6 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|
@ -1746,15 +1734,6 @@
|
||||||
"optional": true
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
package.json
25
package.json
|
|
@ -1,26 +1,31 @@
|
||||||
{
|
{
|
||||||
"name": "palico-bot",
|
"name": "palico-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Bot Discord et jukebox web autohébergé",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --env-file=.env index.js",
|
"start": "node --env-file=.env index.js",
|
||||||
"deploy": "node --env-file=.env deployCommands.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"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/voice": "^0.19.2",
|
"@discordjs/voice": "^0.19.2",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.26.4",
|
||||||
"express": "^5.1.0",
|
"express": "^5.2.1",
|
||||||
"libsodium-wrappers": "^0.7.15",
|
"express-rate-limit": "^8.5.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"helmet": "^8.2.0",
|
||||||
"opusscript": "^0.0.8",
|
"multer": "^2.2.0",
|
||||||
"uuid": "^11.0.3"
|
"opusscript": "^0.0.8"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"undici": "^6.27.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
<div class="panel panel--glass">
|
<div class="panel panel--glass">
|
||||||
<h2>Authentification</h2>
|
<h2>Authentification</h2>
|
||||||
<p class="panel__subtitle">
|
<p class="panel__subtitle">
|
||||||
Ca sera stocké sur ton navigateur si tu sauvegardes !
|
La clé reste dans cet onglet jusqu'à sa fermeture.
|
||||||
</p>
|
</p>
|
||||||
<form id="settingsForm">
|
<form id="settingsForm">
|
||||||
<div class="grid grid--two">
|
<div class="grid grid--two">
|
||||||
|
|
@ -42,17 +42,17 @@
|
||||||
id="displayName"
|
id="displayName"
|
||||||
name="displayName"
|
name="displayName"
|
||||||
placeholder="Shinuwa"
|
placeholder="Shinuwa"
|
||||||
autocomplete="off"
|
autocomplete="nickname"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="apiKey">Mot de passe</label>
|
<label for="apiKey">Mot de passe</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="password"
|
||||||
id="apiKey"
|
id="apiKey"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
placeholder="Mot de passe"
|
placeholder="Mot de passe"
|
||||||
autocomplete="off"
|
autocomplete="current-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -171,7 +171,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn--ghost btn--small"
|
class="btn btn--ghost btn--small"
|
||||||
onclick="window.location.href = '/'"
|
id="backHomeBtn"
|
||||||
>
|
>
|
||||||
Retour à l'accueil
|
Retour à l'accueil
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ const API_KEY_STORAGE_KEY = "palico-api-key";
|
||||||
const DISPLAY_NAME_STORAGE_KEY = "palico-display-name";
|
const DISPLAY_NAME_STORAGE_KEY = "palico-display-name";
|
||||||
|
|
||||||
export function getStoredApiKey() {
|
export function getStoredApiKey() {
|
||||||
return localStorage.getItem(API_KEY_STORAGE_KEY) || "";
|
return sessionStorage.getItem(API_KEY_STORAGE_KEY) || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setStoredApiKey(value) {
|
export function setStoredApiKey(value) {
|
||||||
localStorage.setItem(API_KEY_STORAGE_KEY, value || "");
|
sessionStorage.setItem(API_KEY_STORAGE_KEY, value || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStoredDisplayName() {
|
export function getStoredDisplayName() {
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,11 @@ const elements = {
|
||||||
heroQueueBtn: document.getElementById("heroQueueBtn"),
|
heroQueueBtn: document.getElementById("heroQueueBtn"),
|
||||||
uploadSection: document.getElementById("uploadSection"),
|
uploadSection: document.getElementById("uploadSection"),
|
||||||
queueSection: document.getElementById("queueSection"),
|
queueSection: document.getElementById("queueSection"),
|
||||||
|
backHomeBtn: document.getElementById("backHomeBtn"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pollIntervalId = null;
|
let pollIntervalId = null;
|
||||||
|
let refreshPromise = null;
|
||||||
|
|
||||||
const { setStatus, clearStatus } = createStatusManager(elements.status);
|
const { setStatus, clearStatus } = createStatusManager(elements.status);
|
||||||
const apiFetch = createApiClient({
|
const apiFetch = createApiClient({
|
||||||
|
|
@ -51,7 +53,18 @@ const apiFetch = createApiClient({
|
||||||
getApiKey: () => state.apiKey,
|
getApiKey: () => state.apiKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function refreshAll({ silent = false } = {}) {
|
function refreshAll(options = {}) {
|
||||||
|
if (refreshPromise) {
|
||||||
|
return refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshPromise = performRefresh(options).finally(() => {
|
||||||
|
refreshPromise = null;
|
||||||
|
});
|
||||||
|
return refreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performRefresh({ silent = false } = {}) {
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
clearStatus();
|
clearStatus();
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +306,9 @@ function bindEvents() {
|
||||||
elements.heroQueueBtn?.addEventListener("click", () =>
|
elements.heroQueueBtn?.addEventListener("click", () =>
|
||||||
elements.queueSection?.scrollIntoView({ behavior: "smooth" })
|
elements.queueSection?.scrollIntoView({ behavior: "smooth" })
|
||||||
);
|
);
|
||||||
|
elements.backHomeBtn?.addEventListener("click", () => {
|
||||||
|
window.location.href = "/";
|
||||||
|
});
|
||||||
|
|
||||||
elements.playerButtons?.forEach((button) => {
|
elements.playerButtons?.forEach((button) => {
|
||||||
button.addEventListener("click", async () => {
|
button.addEventListener("click", async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,417 +0,0 @@
|
||||||
: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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
scripts/check-syntax.js
Normal file
31
scripts/check-syntax.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
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`);
|
||||||
|
|
@ -10,12 +10,16 @@ const command = {
|
||||||
option
|
option
|
||||||
.setName("number")
|
.setName("number")
|
||||||
.setDescription("Nombre de dés a lancer")
|
.setDescription("Nombre de dés a lancer")
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(20)
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
)
|
)
|
||||||
.addIntegerOption((option) =>
|
.addIntegerOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("faces")
|
.setName("faces")
|
||||||
.setDescription("Nombre de faces sur le dé")
|
.setDescription("Nombre de faces sur le dé")
|
||||||
|
.setMinValue(2)
|
||||||
|
.setMaxValue(1000)
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
),
|
),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
|
|
@ -26,14 +30,6 @@ const command = {
|
||||||
var tails = 0;
|
var tails = 0;
|
||||||
var heads = 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) {
|
if (faces === 2) {
|
||||||
message = message + subtext(`Pile = 1 et Face = 2\n`);
|
message = message + subtext(`Pile = 1 et Face = 2\n`);
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +43,7 @@ const command = {
|
||||||
sum = sum + random;
|
sum = sum + random;
|
||||||
|
|
||||||
message = message + `[ ${random} ] ${i === number - 1 ? "\n" : " "}`;
|
message = message + `[ ${random} ] ${i === number - 1 ? "\n" : " "}`;
|
||||||
interaction.editReply(message);
|
await interaction.editReply(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (number > 1 && faces > 2) {
|
if (number > 1 && faces > 2) {
|
||||||
|
|
@ -64,7 +60,7 @@ const command = {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interaction.editReply(message);
|
await interaction.editReply(message);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,40 @@ const command = {
|
||||||
option
|
option
|
||||||
.setName("number")
|
.setName("number")
|
||||||
.setDescription("Nombre d'équipe")
|
.setDescription("Nombre d'équipe")
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(20)
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
)
|
)
|
||||||
.addStringOption((option) =>
|
.addStringOption((option) =>
|
||||||
option
|
option
|
||||||
.setName("players")
|
.setName("players")
|
||||||
.setDescription("Liste des joueurs, séparés par un espace")
|
.setDescription("Liste des joueurs, séparés par un espace")
|
||||||
|
.setMinLength(1)
|
||||||
|
.setMaxLength(1000)
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
),
|
),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
const teamNumber = interaction.options.getInteger("number");
|
const teamNumber = interaction.options.getInteger("number");
|
||||||
const players = interaction.options.getString("players");
|
const players = interaction.options.getString("players");
|
||||||
const playersArray = players.split(" ");
|
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 teams = Array.from({ length: teamNumber }, () => []);
|
||||||
const shuffledPlayers = [...playersArray].sort(() => Math.random() - 0.5);
|
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],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
shuffledPlayers.forEach((player, index) => {
|
shuffledPlayers.forEach((player, index) => {
|
||||||
const teamIndex = index % teamNumber;
|
const teamIndex = index % teamNumber;
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,31 @@
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||||
|
import { requirePermission } from "../../features/permissions.js";
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("deletemsgs")
|
.setName("deletemsgs")
|
||||||
.setDescription("Supprime tous les messages, sauf ceux épinglés"),
|
.setDescription("Supprime tous les messages, sauf ceux épinglés")
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
let textChannel = interaction.client.channels.cache.get(
|
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageMessages))) {
|
||||||
interaction.channelId
|
return;
|
||||||
);
|
}
|
||||||
|
|
||||||
textChannel.messages
|
const textChannel = interaction.channel;
|
||||||
.fetch({ limit: 100 })
|
if (!textChannel?.isTextBased() || !textChannel.messages) {
|
||||||
.then((fetched) => {
|
await interaction.reply({
|
||||||
const notPinned = fetched.filter((fetchedMsg) => !fetchedMsg.pinned);
|
content: "Cette commande doit être utilisée dans un salon textuel.",
|
||||||
textChannel.bulkDelete(notPinned, true);
|
ephemeral: 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);
|
|
||||||
});
|
});
|
||||||
|
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) ! 🐱`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ const command = {
|
||||||
.setName("ping")
|
.setName("ping")
|
||||||
.setDescription("Reponds avec Pong"),
|
.setDescription("Reponds avec Pong"),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
console.log(interaction);
|
|
||||||
await interaction.reply({ content: "Euh... Pong ! 🐱", ephemeral: true });
|
await interaction.reply({ content: "Euh... Pong ! 🐱", ephemeral: true });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||||
import { changeVoiceStatus } from "../../features/utilities.js";
|
import { changeVoiceStatus } from "../../features/utilities.js";
|
||||||
|
import { requirePermission } from "../../features/permissions.js";
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("deafall")
|
.setName("deafall")
|
||||||
.setDescription("Deaf tout le monde dans le lounge"),
|
.setDescription("Deaf tout le monde dans le lounge")
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.DeafenMembers),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
changeVoiceStatus(interaction.client, "deaf", "all");
|
if (!(await requirePermission(interaction, PermissionFlagsBits.DeafenMembers))) return;
|
||||||
|
await changeVoiceStatus(interaction.client, "deaf", "all");
|
||||||
await interaction.reply("On n'entends plus rien ! 🐱");
|
await interaction.reply("On n'entends plus rien ! 🐱");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||||
import { changeVoiceStatus } from "../../features/utilities.js";
|
import { changeVoiceStatus } from "../../features/utilities.js";
|
||||||
|
import { requirePermission } from "../../features/permissions.js";
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("muteall")
|
.setName("muteall")
|
||||||
.setDescription("Mute tout le monde dans le lounge"),
|
.setDescription("Mute tout le monde dans le lounge")
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.MuteMembers),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
changeVoiceStatus(interaction.client, "mute", "all");
|
if (!(await requirePermission(interaction, PermissionFlagsBits.MuteMembers))) return;
|
||||||
|
await changeVoiceStatus(interaction.client, "mute", "all");
|
||||||
await interaction.reply("Voila, tout le monde est mioute ! 🐱");
|
await interaction.reply("Voila, tout le monde est mioute ! 🐱");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||||
import { changeVoiceStatus } from "../../features/utilities.js";
|
import { changeVoiceStatus } from "../../features/utilities.js";
|
||||||
|
import { requirePermission } from "../../features/permissions.js";
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("undeafall")
|
.setName("undeafall")
|
||||||
.setDescription("Undeaf tout le monde dans le lounge"),
|
.setDescription("Undeaf tout le monde dans le lounge")
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.DeafenMembers),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
changeVoiceStatus(interaction.client, "undeaf", "all");
|
if (!(await requirePermission(interaction, PermissionFlagsBits.DeafenMembers))) return;
|
||||||
|
await changeVoiceStatus(interaction.client, "undeaf", "all");
|
||||||
await interaction.reply("Voila, les oreilles sont réparées ! 🐱");
|
await interaction.reply("Voila, les oreilles sont réparées ! 🐱");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
|
||||||
import { changeVoiceStatus } from "../../features/utilities.js";
|
import { changeVoiceStatus } from "../../features/utilities.js";
|
||||||
|
import { requirePermission } from "../../features/permissions.js";
|
||||||
|
|
||||||
const command = {
|
const command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("unmuteall")
|
.setName("unmuteall")
|
||||||
.setDescription("Unmute tout le monde dans le lounge"),
|
.setDescription("Unmute tout le monde dans le lounge")
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.MuteMembers),
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
changeVoiceStatus(interaction.client, "unmute", "all");
|
if (!(await requirePermission(interaction, PermissionFlagsBits.MuteMembers))) return;
|
||||||
|
await changeVoiceStatus(interaction.client, "unmute", "all");
|
||||||
await interaction.reply("Et.... unmioute ! 🐱");
|
await interaction.reply("Et.... unmioute ! 🐱");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
const requiredVariables = [
|
const requiredVariables = [
|
||||||
"NODE_ENV",
|
"NODE_ENV",
|
||||||
"LOCATION",
|
|
||||||
"BOT_OWNER",
|
|
||||||
"DISCORD_GUILD_ID",
|
"DISCORD_GUILD_ID",
|
||||||
"SHINUWA_USER_ID",
|
"SHINUWA_USER_ID",
|
||||||
"INVITE_LINK",
|
|
||||||
"WEB_SERVER_PORT",
|
"WEB_SERVER_PORT",
|
||||||
"WEB_API_KEY",
|
"WEB_API_KEY",
|
||||||
"DISCORD_APPLICATION_ID",
|
"DISCORD_APPLICATION_ID",
|
||||||
|
|
@ -26,17 +23,18 @@ if (missingVariables.length > 0) {
|
||||||
|
|
||||||
const webServerPort = Number(process.env.WEB_SERVER_PORT);
|
const webServerPort = Number(process.env.WEB_SERVER_PORT);
|
||||||
|
|
||||||
if (!Number.isInteger(webServerPort) || webServerPort <= 0) {
|
if (
|
||||||
throw new Error("WEB_SERVER_PORT doit être un entier positif");
|
!Number.isInteger(webServerPort) ||
|
||||||
|
webServerPort <= 0 ||
|
||||||
|
webServerPort > 65_535
|
||||||
|
) {
|
||||||
|
throw new Error("WEB_SERVER_PORT doit être compris entre 1 et 65535");
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = Object.freeze({
|
const config = Object.freeze({
|
||||||
environnement: process.env.NODE_ENV,
|
environnement: process.env.NODE_ENV,
|
||||||
location: process.env.LOCATION,
|
|
||||||
botOwner: process.env.BOT_OWNER,
|
|
||||||
discordGuildId: process.env.DISCORD_GUILD_ID,
|
discordGuildId: process.env.DISCORD_GUILD_ID,
|
||||||
shinuwaUserId: process.env.SHINUWA_USER_ID,
|
shinuwaUserId: process.env.SHINUWA_USER_ID,
|
||||||
inviteLink: process.env.INVITE_LINK,
|
|
||||||
webServerPort,
|
webServerPort,
|
||||||
webApiKey: process.env.WEB_API_KEY,
|
webApiKey: process.env.WEB_API_KEY,
|
||||||
applicationId: process.env.DISCORD_APPLICATION_ID,
|
applicationId: process.env.DISCORD_APPLICATION_ID,
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ const event = {
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
const command = interaction.client.commands.get(
|
const command = interaction.client.commands.get(interaction.commandName)?.command;
|
||||||
interaction.commandName
|
|
||||||
).command;
|
|
||||||
|
|
||||||
if (!command) {
|
if (!command) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,19 @@
|
||||||
import { Events } from "discord.js";
|
import { Events } from "discord.js";
|
||||||
import { config } from "../config.js";
|
|
||||||
import { showGameServers } from "../features/showGameServers.js";
|
import { showGameServers } from "../features/showGameServers.js";
|
||||||
import { sendMessage } from "../features/sendMessage.js";
|
|
||||||
import { isConnectedToVoiceChannel } from "../features/utilities.js";
|
|
||||||
|
|
||||||
const event = {
|
const event = {
|
||||||
name: Events.ClientReady,
|
name: Events.ClientReady,
|
||||||
once: true,
|
once: true,
|
||||||
execute(client) {
|
async execute(client) {
|
||||||
console.log(
|
console.log(
|
||||||
`[${new Date().toLocaleString()}] Logged in as ${client.user.tag}`
|
`[${new Date().toLocaleString()}] Logged in as ${client.user.tag}`
|
||||||
);
|
);
|
||||||
|
|
||||||
showGameServers(
|
try {
|
||||||
client,
|
await showGameServers(client);
|
||||||
config.textChannelId,
|
} catch (error) {
|
||||||
config.gameServersMessageId,
|
console.error("Unable to initialize game server status", error);
|
||||||
config.shinuwaUserId
|
}
|
||||||
);
|
|
||||||
|
|
||||||
//sendMessage(client, 'jukebox')
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
13
src/features/permissions.js
Normal file
13
src/features/permissions.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
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 };
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
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 };
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { getJSONData } from "./utilities.js";
|
||||||
|
|
||||||
var currentStatus = [];
|
var currentStatus = [];
|
||||||
|
|
||||||
function checkServersStatus(client) {
|
async function checkServersStatus(client) {
|
||||||
const data = getJSONData("src/static/gameservers-list.json");
|
const data = getJSONData("src/static/gameservers-list.json");
|
||||||
const newStatus = data.servers.map((server) => server.active);
|
const newStatus = data.servers.map((server) => server.active);
|
||||||
|
|
||||||
|
|
@ -11,12 +11,12 @@ function checkServersStatus(client) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMessage(client, data);
|
await updateMessage(client, data);
|
||||||
currentStatus = newStatus;
|
currentStatus = newStatus;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMessage(client, data) {
|
async function updateMessage(client, data) {
|
||||||
var newMessage = `Hello 😺 ! Voila les serveurs de jeux qu'on a chez nous :\n\n`;
|
var newMessage = `Hello 😺 ! Voila les serveurs de jeux qu'on a chez nous :\n\n`;
|
||||||
|
|
||||||
data.servers.forEach((server) => {
|
data.servers.forEach((server) => {
|
||||||
|
|
@ -29,36 +29,33 @@ function updateMessage(client, data) {
|
||||||
"```";
|
"```";
|
||||||
});
|
});
|
||||||
|
|
||||||
newMessage =
|
|
||||||
newMessage +
|
|
||||||
"\nAh d'ailleurs ! Si le serveur est sécurisé, le mot de passe c'est surement **0117**.";
|
|
||||||
newMessage =
|
newMessage =
|
||||||
newMessage +
|
newMessage +
|
||||||
`\nPour mettre en ligne un serveur, faut voir avec <@${config.shinuwaUserId}> , c'est lui qui gère ca !\n`;
|
`\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);
|
const channel = client.channels.cache.get(config.textChannelId);
|
||||||
|
if (!channel?.isTextBased()) {
|
||||||
|
throw new Error("Salon textuel configuré introuvable");
|
||||||
|
}
|
||||||
|
|
||||||
channel.messages
|
const message = await channel.messages.fetch(config.gameServersMessageId);
|
||||||
.fetch(config.gameServersMessageId)
|
await message.edit(newMessage);
|
||||||
.then((message) => {
|
|
||||||
message
|
|
||||||
.edit(newMessage)
|
|
||||||
.then(
|
|
||||||
console.log(
|
console.log(
|
||||||
`[${new Date().toLocaleString()}] showGameServers: Game servers message updated`
|
`[${new Date().toLocaleString()}] showGameServers: Game servers message updated`
|
||||||
)
|
);
|
||||||
)
|
|
||||||
.catch(console.error);
|
|
||||||
})
|
|
||||||
.catch(console.error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGameServers(client) {
|
async function showGameServers(client) {
|
||||||
const data = getJSONData("src/static/gameservers-list.json");
|
const data = getJSONData("src/static/gameservers-list.json");
|
||||||
currentStatus = data.servers.map((server) => server.active);
|
currentStatus = data.servers.map((server) => server.active);
|
||||||
|
|
||||||
updateMessage(client, data);
|
await updateMessage(client, data);
|
||||||
setInterval(checkServersStatus, 120000, client);
|
const interval = setInterval(() => {
|
||||||
|
checkServersStatus(client).catch((error) =>
|
||||||
|
console.error("Unable to update game server status", error)
|
||||||
|
);
|
||||||
|
}, 120000);
|
||||||
|
interval.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
export { showGameServers };
|
export { showGameServers };
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,51 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
|
|
||||||
function changeVoiceStatus(client, status, members) {
|
async function changeVoiceStatus(client, status, members) {
|
||||||
const changeStatus = (type, value, members) => {
|
const changeStatus = async (type, value, selectedMembers) => {
|
||||||
|
const operations = [];
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "mute":
|
case "mute":
|
||||||
for (let member of members) {
|
for (const member of selectedMembers.values()) {
|
||||||
member[1].voice.setMute(value);
|
operations.push(member.voice.setMute(value));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "deaf":
|
case "deaf":
|
||||||
for (let member of members) {
|
for (const member of selectedMembers.values()) {
|
||||||
member[1].voice.setDeaf(value);
|
operations.push(member.voice.setDeaf(value));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
await Promise.all(operations);
|
||||||
};
|
};
|
||||||
|
|
||||||
var voiceChannel = client.channels.cache.get(config.voiceChannelId);
|
const voiceChannel = client.channels.cache.get(config.voiceChannelId);
|
||||||
var connectedMembers = voiceChannel.members;
|
if (!voiceChannel?.isVoiceBased()) {
|
||||||
|
throw new Error("Salon vocal configuré introuvable");
|
||||||
|
}
|
||||||
|
const connectedMembers = voiceChannel.members.filter(
|
||||||
|
(member) => !member.user.bot
|
||||||
|
);
|
||||||
|
|
||||||
if (members == "all") {
|
if (members === "all") {
|
||||||
members = connectedMembers;
|
members = connectedMembers;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "mute":
|
case "mute":
|
||||||
changeStatus("mute", true, members);
|
await changeStatus("mute", true, members);
|
||||||
break;
|
break;
|
||||||
case "unmute":
|
case "unmute":
|
||||||
changeStatus("mute", false, members);
|
await changeStatus("mute", false, members);
|
||||||
break;
|
break;
|
||||||
case "deaf":
|
case "deaf":
|
||||||
changeStatus("deaf", true, members);
|
await changeStatus("deaf", true, members);
|
||||||
break;
|
break;
|
||||||
case "undeaf":
|
case "undeaf":
|
||||||
changeStatus("deaf", false, members);
|
await changeStatus("deaf", false, members);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Statut vocal inconnu : ${status}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,24 +60,8 @@ function getRandomInt(max) {
|
||||||
return Math.floor(Math.random() * max);
|
return Math.floor(Math.random() * max);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export {
|
||||||
changeVoiceStatus,
|
changeVoiceStatus,
|
||||||
getJSONData,
|
getJSONData,
|
||||||
getRandomInt,
|
getRandomInt,
|
||||||
isConnectedToVoiceChannel,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
35
src/media/validateAudioFile.js
Normal file
35
src/media/validateAudioFile.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
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 };
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
} from "@discordjs/voice";
|
} from "@discordjs/voice";
|
||||||
import { Events } from "discord.js";
|
import { Events } from "discord.js";
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
|
import { serializeTrack, serializeTracks } from "../web/serializers.js";
|
||||||
import {
|
import {
|
||||||
deleteTrackFile,
|
deleteTrackFile,
|
||||||
handleTrackCompletion,
|
handleTrackCompletion,
|
||||||
|
|
@ -29,6 +30,8 @@ class MusicQueue extends EventEmitter {
|
||||||
this.idleDisconnectTimeout = null;
|
this.idleDisconnectTimeout = null;
|
||||||
this.aloneDisconnectTimeout = null;
|
this.aloneDisconnectTimeout = null;
|
||||||
this.suspendedForAlone = false;
|
this.suspendedForAlone = false;
|
||||||
|
this.playNextPromise = null;
|
||||||
|
this.shuttingDown = false;
|
||||||
this.audioPlayer = createAudioPlayer({
|
this.audioPlayer = createAudioPlayer({
|
||||||
behaviors: {
|
behaviors: {
|
||||||
noSubscriber: NoSubscriberBehavior.Play,
|
noSubscriber: NoSubscriberBehavior.Play,
|
||||||
|
|
@ -49,13 +52,18 @@ class MusicQueue extends EventEmitter {
|
||||||
handleTrackCompletion(this.currentTrack);
|
handleTrackCompletion(this.currentTrack);
|
||||||
}
|
}
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
this.playNext();
|
if (!this.shuttingDown) {
|
||||||
|
void this.playNext();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.audioPlayer.on("error", (error) => {
|
this.audioPlayer.on("error", (error) => {
|
||||||
console.error("Audio player encountered an error", error);
|
console.error("Audio player encountered an error", error);
|
||||||
|
handleTrackCompletion(this.currentTrack);
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
this.playNext();
|
if (!this.shuttingDown) {
|
||||||
|
void this.playNext();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
this.audioPlayer.on("stateChange", (oldState, newState) => {
|
||||||
|
|
@ -66,20 +74,29 @@ class MusicQueue extends EventEmitter {
|
||||||
|
|
||||||
snapshot() {
|
snapshot() {
|
||||||
return {
|
return {
|
||||||
current: this.currentTrack,
|
current: serializeTrack(this.currentTrack),
|
||||||
upcoming: this.queue,
|
upcoming: serializeTracks(this.queue),
|
||||||
status: this.playbackStatus,
|
status: this.playbackStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureConnection() {
|
async ensureConnection() {
|
||||||
|
if (this.connection) {
|
||||||
if (
|
if (
|
||||||
this.connection &&
|
this.connection.joinConfig.channelId === config.voiceChannelId &&
|
||||||
this.connection.joinConfig.channelId === config.voiceChannelId
|
this.connection.state.status === VoiceConnectionStatus.Ready
|
||||||
) {
|
) {
|
||||||
return this.connection;
|
return this.connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.connection.destroy();
|
||||||
|
} catch {
|
||||||
|
// The connection may already be destroyed.
|
||||||
|
}
|
||||||
|
this.connection = null;
|
||||||
|
}
|
||||||
|
|
||||||
let channel = this.client.channels.cache.get(config.voiceChannelId);
|
let channel = this.client.channels.cache.get(config.voiceChannelId);
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
|
|
@ -104,7 +121,32 @@ class MusicQueue extends EventEmitter {
|
||||||
|
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
connection.subscribe(this.audioPlayer);
|
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);
|
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;
|
||||||
|
}
|
||||||
this.evaluateAloneStatus();
|
this.evaluateAloneStatus();
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
@ -114,13 +156,24 @@ class MusicQueue extends EventEmitter {
|
||||||
this.cancelIdleDisconnect();
|
this.cancelIdleDisconnect();
|
||||||
this.emit("queueUpdate", this.snapshot());
|
this.emit("queueUpdate", this.snapshot());
|
||||||
if (!this.currentTrack) {
|
if (!this.currentTrack) {
|
||||||
return this.playNext();
|
void this.playNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
async playNext() {
|
playNext() {
|
||||||
|
if (this.playNextPromise) {
|
||||||
|
return this.playNextPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.playNextPromise = this.runPlayNext().finally(() => {
|
||||||
|
this.playNextPromise = null;
|
||||||
|
});
|
||||||
|
return this.playNextPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runPlayNext() {
|
||||||
if (this.suspendedForAlone) {
|
if (this.suspendedForAlone) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -132,32 +185,43 @@ class MusicQueue extends EventEmitter {
|
||||||
}
|
}
|
||||||
this.cancelIdleDisconnect();
|
this.cancelIdleDisconnect();
|
||||||
|
|
||||||
const nextTrack = this.queue.shift();
|
const nextTrack = this.queue[0];
|
||||||
this.emit("queueUpdate", this.snapshot());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.ensureConnection();
|
await this.ensureConnection();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to connect to the configured voice channel", error);
|
console.error("Unable to connect to the configured voice channel", error);
|
||||||
deleteTrackFile(nextTrack);
|
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.shuttingDown ||
|
||||||
|
this.suspendedForAlone ||
|
||||||
|
this.queue[0]?.id !== nextTrack.id
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.queue.shift();
|
||||||
const stream = fs.createReadStream(nextTrack.path);
|
const stream = fs.createReadStream(nextTrack.path);
|
||||||
const resource = createAudioResource(stream, {
|
const resource = createAudioResource(stream, {
|
||||||
inlineVolume: true,
|
inlineVolume: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentTrack = nextTrack;
|
this.currentTrack = {
|
||||||
|
...nextTrack,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
this.audioPlayer.play(resource);
|
this.audioPlayer.play(resource);
|
||||||
this.emit("trackStart", nextTrack);
|
this.emit("trackStart", this.currentTrack);
|
||||||
|
this.emit("queueUpdate", this.snapshot());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unable to play track", error);
|
console.error("Unable to play track", error);
|
||||||
deleteTrackFile(nextTrack);
|
deleteTrackFile(nextTrack);
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
return this.playNext();
|
return this.runPlayNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,7 +258,7 @@ class MusicQueue extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
skip() {
|
skip() {
|
||||||
this.audioPlayer.stop(true);
|
return this.audioPlayer.stop(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
|
|
@ -262,6 +326,9 @@ class MusicQueue extends EventEmitter {
|
||||||
this.cancelIdleDisconnect();
|
this.cancelIdleDisconnect();
|
||||||
this.cancelAloneDisconnect();
|
this.cancelAloneDisconnect();
|
||||||
|
|
||||||
|
const interruptedTrack = this.currentTrack;
|
||||||
|
this.currentTrack = null;
|
||||||
|
|
||||||
if (this.connection) {
|
if (this.connection) {
|
||||||
try {
|
try {
|
||||||
this.connection.destroy();
|
this.connection.destroy();
|
||||||
|
|
@ -277,7 +344,7 @@ class MusicQueue extends EventEmitter {
|
||||||
console.error("Failed to stop audio player", error);
|
console.error("Failed to stop audio player", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentTrack = null;
|
handleTrackCompletion(interruptedTrack);
|
||||||
this.emit("queueUpdate", this.snapshot());
|
this.emit("queueUpdate", this.snapshot());
|
||||||
console.log(
|
console.log(
|
||||||
`[${new Date().toLocaleString()}] MusicQueue disconnected (${reason})`
|
`[${new Date().toLocaleString()}] MusicQueue disconnected (${reason})`
|
||||||
|
|
@ -342,7 +409,7 @@ class MusicQueue extends EventEmitter {
|
||||||
this.suspendedForAlone = false;
|
this.suspendedForAlone = false;
|
||||||
this.cancelAloneDisconnect();
|
this.cancelAloneDisconnect();
|
||||||
if (wasSuspended && !this.currentTrack && this.queue.length > 0) {
|
if (wasSuspended && !this.currentTrack && this.queue.length > 0) {
|
||||||
this.playNext();
|
void this.playNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -365,6 +432,17 @@ class MusicQueue extends EventEmitter {
|
||||||
const others = channel.members.filter((member) => member.id !== clientId);
|
const others = channel.members.filter((member) => member.id !== clientId);
|
||||||
return others.size === 0;
|
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 };
|
export { MusicQueue };
|
||||||
|
|
|
||||||
10
src/static/gameservers-list.example.json
Normal file
10
src/static/gameservers-list.example.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"game": "Nom du jeu",
|
||||||
|
"address": "games.example.com:25565",
|
||||||
|
"port": 25565,
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import { config } from "../../config.js";
|
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) {
|
function authMiddleware(req, res, next) {
|
||||||
if (!config.webApiKey) {
|
if (!config.webApiKey) {
|
||||||
|
|
@ -6,9 +11,10 @@ function authMiddleware(req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = req.headers.authorization || "";
|
const header = req.headers.authorization || "";
|
||||||
const token = header.replace("Bearer ", "");
|
const match = /^Bearer ([^\s]+)$/.exec(header);
|
||||||
|
const token = match?.[1] || "";
|
||||||
|
|
||||||
if (token === config.webApiKey) {
|
if (timingSafeEqual(tokenDigest(token), tokenDigest(config.webApiKey))) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ import {
|
||||||
findPermanentTrack,
|
findPermanentTrack,
|
||||||
getPermanentLibrary,
|
getPermanentLibrary,
|
||||||
} from "../storage/storageManager.js";
|
} from "../storage/storageManager.js";
|
||||||
|
import { serializeTrack, serializeTracks } from "../serializers.js";
|
||||||
|
|
||||||
function libraryRouter({ queue }) {
|
function libraryRouter({ queue }) {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/", (req, res) => {
|
router.get("/", (req, res) => {
|
||||||
res.json({ tracks: getPermanentLibrary() });
|
res.json({ tracks: serializeTracks(getPermanentLibrary()) });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/:libraryId", (req, res) => {
|
router.delete("/:libraryId", (req, res) => {
|
||||||
|
|
@ -21,7 +22,7 @@ function libraryRouter({ queue }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.removeByLibraryId(track.id);
|
queue.removeByLibraryId(track.id);
|
||||||
res.json({ removed: track });
|
res.json({ removed: serializeTrack(track) });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/:libraryId/enqueue", (req, res) => {
|
router.post("/:libraryId/enqueue", (req, res) => {
|
||||||
|
|
@ -35,7 +36,7 @@ function libraryRouter({ queue }) {
|
||||||
req.body?.requestedBy
|
req.body?.requestedBy
|
||||||
);
|
);
|
||||||
queue.enqueue(queueTrack);
|
queue.enqueue(queueTrack);
|
||||||
res.status(201).json({ track: queueTrack });
|
res.status(201).json({ track: serializeTrack(queueTrack) });
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { createQueueTrackFromLibrary, findPermanentTrack } from "../storage/storageManager.js";
|
import { createQueueTrackFromLibrary, findPermanentTrack } from "../storage/storageManager.js";
|
||||||
|
import { serializeTrack } from "../serializers.js";
|
||||||
|
|
||||||
function queueRouter({ queue }) {
|
function queueRouter({ queue }) {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
@ -9,7 +10,7 @@ function queueRouter({ queue }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/", (req, res) => {
|
router.post("/", (req, res) => {
|
||||||
const { libraryTrackId, requestedBy } = req.body;
|
const { libraryTrackId, requestedBy } = req.body || {};
|
||||||
|
|
||||||
if (!libraryTrackId) {
|
if (!libraryTrackId) {
|
||||||
return res.status(400).json({ error: "libraryTrackId manquant" });
|
return res.status(400).json({ error: "libraryTrackId manquant" });
|
||||||
|
|
@ -23,7 +24,7 @@ function queueRouter({ queue }) {
|
||||||
|
|
||||||
const track = createQueueTrackFromLibrary(libraryTrack, requestedBy);
|
const track = createQueueTrackFromLibrary(libraryTrack, requestedBy);
|
||||||
queue.enqueue(track);
|
queue.enqueue(track);
|
||||||
res.status(201).json({ track });
|
res.status(201).json({ track: serializeTrack(track) });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.delete("/:trackId", async (req, res) => {
|
router.delete("/:trackId", async (req, res) => {
|
||||||
|
|
@ -32,13 +33,13 @@ function queueRouter({ queue }) {
|
||||||
return res.status(404).json({ error: "Piste introuvable" });
|
return res.status(404).json({ error: "Piste introuvable" });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ removed });
|
res.json({ removed: serializeTrack(removed) });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.patch("/:trackId/move", (req, res) => {
|
router.patch("/:trackId/move", (req, res) => {
|
||||||
const position = Number(req.body?.position);
|
const position = Number(req.body?.position);
|
||||||
if (Number.isNaN(position)) {
|
if (!Number.isInteger(position) || position < 0) {
|
||||||
return res.status(400).json({ error: "position doit être un nombre" });
|
return res.status(400).json({ error: "position doit être un entier positif" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = queue.move(req.params.trackId, position);
|
const snapshot = queue.move(req.params.trackId, position);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,41 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import { TEMP_DIR, registerUploadedFile } from "../storage/storageManager.js";
|
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",
|
||||||
|
]);
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
dest: TEMP_DIR,
|
dest: TEMP_DIR,
|
||||||
limits: {
|
limits: {
|
||||||
fileSize: 150 * 1024 * 1024,
|
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));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -19,17 +49,41 @@ function uploadsRouter({ queue }) {
|
||||||
|
|
||||||
const permanent = req.body?.permanent === "true";
|
const permanent = req.body?.permanent === "true";
|
||||||
const enqueue = req.body?.enqueue !== "false";
|
const enqueue = req.body?.enqueue !== "false";
|
||||||
const uploader = req.body?.uploader || "web-ui";
|
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);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const track = registerUploadedFile(req.file, { permanent, uploader });
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
if (enqueue) {
|
if (enqueue) {
|
||||||
queue.enqueue(track);
|
queue.enqueue(track);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(201).json({ track, enqueued: enqueue });
|
return res.status(201).json({
|
||||||
|
track: serializeTrack(track),
|
||||||
|
enqueued: enqueue,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
discardUploadedFile(req.file);
|
||||||
return next(error);
|
return next(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
14
src/web/serializers.js
Normal file
14
src/web/serializers.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
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 };
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import helmet from "helmet";
|
||||||
|
import { rateLimit } from "express-rate-limit";
|
||||||
|
import multer from "multer";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
import { authMiddleware } from "./middlewares/auth.js";
|
import { authMiddleware } from "./middlewares/auth.js";
|
||||||
|
|
@ -6,19 +9,56 @@ import { libraryRouter } from "./routes/library.js";
|
||||||
import { playerRouter } from "./routes/player.js";
|
import { playerRouter } from "./routes/player.js";
|
||||||
import { queueRouter } from "./routes/queue.js";
|
import { queueRouter } from "./routes/queue.js";
|
||||||
import { uploadsRouter } from "./routes/uploads.js";
|
import { uploadsRouter } from "./routes/uploads.js";
|
||||||
import { ensureStorageLayout } from "./storage/storageManager.js";
|
import {
|
||||||
|
cleanupTemporaryFiles,
|
||||||
|
ensureStorageLayout,
|
||||||
|
} from "./storage/storageManager.js";
|
||||||
|
|
||||||
const PUBLIC_DIR = path.join(process.cwd(), "public");
|
const PUBLIC_DIR = path.join(process.cwd(), "public");
|
||||||
|
|
||||||
function startWebServer(dependencies) {
|
function startWebServer(dependencies) {
|
||||||
ensureStorageLayout();
|
ensureStorageLayout();
|
||||||
|
cleanupTemporaryFiles();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.disable("x-powered-by");
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(express.json({ limit: "32kb" }));
|
||||||
|
|
||||||
|
app.get("/healthz", (_req, res) => {
|
||||||
|
res.json({ status: "ok" });
|
||||||
|
});
|
||||||
|
|
||||||
const apiRouter = express.Router();
|
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(authMiddleware);
|
||||||
apiRouter.use("/uploads", uploadsRouter(dependencies));
|
apiRouter.use(
|
||||||
|
"/uploads",
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 15 * 60_000,
|
||||||
|
limit: 10,
|
||||||
|
standardHeaders: "draft-8",
|
||||||
|
legacyHeaders: false,
|
||||||
|
}),
|
||||||
|
uploadsRouter(dependencies)
|
||||||
|
);
|
||||||
apiRouter.use("/queue", queueRouter(dependencies));
|
apiRouter.use("/queue", queueRouter(dependencies));
|
||||||
apiRouter.use("/library", libraryRouter(dependencies));
|
apiRouter.use("/library", libraryRouter(dependencies));
|
||||||
apiRouter.use("/player", playerRouter(dependencies));
|
apiRouter.use("/player", playerRouter(dependencies));
|
||||||
|
|
@ -29,15 +69,28 @@ function startWebServer(dependencies) {
|
||||||
);
|
);
|
||||||
app.use(express.static(PUBLIC_DIR));
|
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 port = Number(process.env.PORT || config.webServerPort || 3000);
|
||||||
|
|
||||||
app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
console.log(
|
console.log(
|
||||||
`[${new Date().toLocaleString()}] Web control server listening on port ${port}`
|
`[${new Date().toLocaleString()}] Web control server listening on port ${port}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return { app, server };
|
||||||
}
|
}
|
||||||
|
|
||||||
export { startWebServer };
|
export { startWebServer };
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { v4 as uuid } from "uuid";
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
const STORAGE_ROOT = path.join(process.cwd(), "storage");
|
const STORAGE_ROOT = path.join(process.cwd(), "storage");
|
||||||
const PERMANENT_DIR = path.join(STORAGE_ROOT, "permanent");
|
const PERMANENT_DIR = path.join(STORAGE_ROOT, "permanent");
|
||||||
const TEMP_DIR = path.join(STORAGE_ROOT, "temp");
|
const TEMP_DIR = path.join(STORAGE_ROOT, "temp");
|
||||||
const LIBRARY_FILE = path.join(STORAGE_ROOT, "library.json");
|
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;
|
||||||
|
|
||||||
function ensureStorageLayout() {
|
function ensureStorageLayout() {
|
||||||
if (!fs.existsSync(STORAGE_ROOT)) {
|
if (!fs.existsSync(STORAGE_ROOT)) {
|
||||||
|
|
@ -20,26 +24,71 @@ function ensureStorageLayout() {
|
||||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(RUNTIME_DIR)) {
|
||||||
|
fs.mkdirSync(RUNTIME_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(LIBRARY_FILE)) {
|
if (!fs.existsSync(LIBRARY_FILE)) {
|
||||||
fs.writeFileSync(LIBRARY_FILE, JSON.stringify([]));
|
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`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readLibrary() {
|
function readLibrary() {
|
||||||
try {
|
try {
|
||||||
const raw = fs.readFileSync(LIBRARY_FILE, { encoding: "utf8" });
|
const raw = fs.readFileSync(LIBRARY_FILE, { encoding: "utf8" });
|
||||||
return JSON.parse(raw);
|
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),
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to read library file", error);
|
console.error("Failed to read library file", error);
|
||||||
return [];
|
throw new Error("Impossible de lire la bibliothèque", { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeLibrary(entries) {
|
function writeLibrary(entries) {
|
||||||
fs.writeFileSync(LIBRARY_FILE, JSON.stringify(entries, null, 2));
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerUploadedFile(file, { permanent, uploader = "unknown" }) {
|
function registerUploadedFile(
|
||||||
|
file,
|
||||||
|
{ permanent, uploader = "unknown", title = file.originalname }
|
||||||
|
) {
|
||||||
ensureStorageLayout();
|
ensureStorageLayout();
|
||||||
const createdAt = new Date().toISOString();
|
const createdAt = new Date().toISOString();
|
||||||
let finalPath = file.path;
|
let finalPath = file.path;
|
||||||
|
|
@ -47,13 +96,26 @@ function registerUploadedFile(file, { permanent, uploader = "unknown" }) {
|
||||||
let permanentEntry = null;
|
let permanentEntry = null;
|
||||||
|
|
||||||
if (permanent) {
|
if (permanent) {
|
||||||
const extension = path.extname(file.originalname) || "";
|
const library = readLibrary();
|
||||||
storedName = `${uuid()}${extension}`;
|
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()}`;
|
||||||
finalPath = path.join(PERMANENT_DIR, storedName);
|
finalPath = path.join(PERMANENT_DIR, storedName);
|
||||||
fs.renameSync(file.path, finalPath);
|
fs.renameSync(file.path, finalPath);
|
||||||
permanentEntry = {
|
permanentEntry = {
|
||||||
id: uuid(),
|
id: randomUUID(),
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
|
title,
|
||||||
storedName,
|
storedName,
|
||||||
path: finalPath,
|
path: finalPath,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
|
|
@ -61,14 +123,13 @@ function registerUploadedFile(file, { permanent, uploader = "unknown" }) {
|
||||||
uploader,
|
uploader,
|
||||||
createdAt,
|
createdAt,
|
||||||
};
|
};
|
||||||
const library = readLibrary();
|
|
||||||
library.push(permanentEntry);
|
library.push(permanentEntry);
|
||||||
writeLibrary(library);
|
writeLibrary(library);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: uuid(),
|
id: randomUUID(),
|
||||||
title: file.originalname,
|
title,
|
||||||
originalName: file.originalname,
|
originalName: file.originalname,
|
||||||
path: finalPath,
|
path: finalPath,
|
||||||
storedName,
|
storedName,
|
||||||
|
|
@ -96,8 +157,8 @@ function createQueueTrackFromLibrary(libraryTrack, requestedBy = "library") {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: uuid(),
|
id: randomUUID(),
|
||||||
title: libraryTrack.originalName,
|
title: libraryTrack.title || libraryTrack.originalName,
|
||||||
originalName: libraryTrack.originalName,
|
originalName: libraryTrack.originalName,
|
||||||
path: libraryTrack.path,
|
path: libraryTrack.path,
|
||||||
storedName: libraryTrack.storedName,
|
storedName: libraryTrack.storedName,
|
||||||
|
|
@ -110,6 +171,26 @@ 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) {
|
function deleteFileSafe(filePath) {
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -122,6 +203,10 @@ function deleteFileSafe(filePath) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function discardUploadedFile(file) {
|
||||||
|
deleteFileSafe(file?.path);
|
||||||
|
}
|
||||||
|
|
||||||
function deletePermanentTrack(id) {
|
function deletePermanentTrack(id) {
|
||||||
const library = getPermanentLibrary();
|
const library = getPermanentLibrary();
|
||||||
const index = library.findIndex((entry) => entry.id === id);
|
const index = library.findIndex((entry) => entry.id === id);
|
||||||
|
|
@ -160,6 +245,8 @@ export {
|
||||||
getPermanentLibrary,
|
getPermanentLibrary,
|
||||||
findPermanentTrack,
|
findPermanentTrack,
|
||||||
createQueueTrackFromLibrary,
|
createQueueTrackFromLibrary,
|
||||||
|
cleanupTemporaryFiles,
|
||||||
|
discardUploadedFile,
|
||||||
deletePermanentTrack,
|
deletePermanentTrack,
|
||||||
handleTrackCompletion,
|
handleTrackCompletion,
|
||||||
deleteTrackFile,
|
deleteTrackFile,
|
||||||
|
|
|
||||||
49
test/auth.test.js
Normal file
49
test/auth.test.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
58
test/commands.test.js
Normal file
58
test/commands.test.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
52
test/musicQueue.test.js
Normal file
52
test/musicQueue.test.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
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",
|
||||||
|
});
|
||||||
|
});
|
||||||
21
test/serializers.test.js
Normal file
21
test/serializers.test.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
83
test/storageManager.test.js
Normal file
83
test/storageManager.test.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue