commit 6d2e379a0ceda8977e8b06fadc6bfce5aa7a913f Author: Shinuwa Date: Sun Jun 28 22:44:04 2026 +0200 Initial Commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..cf84bd6 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +NODE_ENV=test +LOCATION= +BOT_OWNER= +DISCORD_GUILD_ID= +SHINUWA_USER_ID= +INVITE_LINK= +WEB_SERVER_PORT=3000 +WEB_API_KEY= +DISCORD_APPLICATION_ID= +DISCORD_TOKEN= +VOICE_CHANNEL_ID= +TEXT_CHANNEL_ID= +GAME_SERVERS_MESSAGE_ID= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..280a034 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +#node +node_modules/ + +# Local configuration and test data +.env +.env.* +!.env.example +src/static/gameservers-list.json + +#storage +storage/permanent/* +!storage/permanent/.gitkeep +storage/temp/* +!storage/temp/.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..d258522 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# palico-bot + +## Configuration + +La configuration sensible est stockée dans deux fichiers exclus de Git : + +- `.env.local` pour le bot de test en local ; +- `.env.production` pour le bot de production sur la VM. + +Le fichier `.env.example` documente les variables attendues sans contenir de +secret. Les fichiers réels doivent rester hors du dépôt. + +```bash +# Lancer le bot local +npm start + +# Lancer le bot sur la VM +npm run start:prod + +# Déployer les commandes locales ou de production +npm run deploy +npm run deploy:prod +``` + +Node.js 20.6 ou supérieur est nécessaire pour la prise en charge native de +`--env-file`. diff --git a/deployCommands.js b/deployCommands.js new file mode 100644 index 0000000..854ab0a --- /dev/null +++ b/deployCommands.js @@ -0,0 +1,56 @@ +import { REST, Routes } from "discord.js"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { config } from "./src/config.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const commands = []; +const foldersPath = path.join(__dirname, "src/commands"); +const commandFolders = fs.readdirSync(foldersPath); + +for (const folder of commandFolders) { + if (folder.indexOf("[disabled]") === -1 || config.environnement === "test") { + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".js")); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const commandFile = await import(filePath); + if ("data" in commandFile.command && "execute" in commandFile.command) { + commands.push(commandFile.command.data.toJSON()); + } else { + console.log( + `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.` + ); + } + } + } +} + +const rest = new REST().setToken(config.token); + +(async () => { + try { + console.log( + `Started refreshing ${commands.length} application (/) commands.` + ); + + const data = await rest.put( + Routes.applicationGuildCommands( + config.applicationId, + config.discordGuildId + ), + { body: commands } + ); + + console.log( + `Successfully reloaded ${data.length} application (/) commands.` + ); + } catch (error) { + console.error(error); + } +})(); diff --git a/index.js b/index.js new file mode 100644 index 0000000..7c5a3af --- /dev/null +++ b/index.js @@ -0,0 +1,64 @@ +import { Client, Collection, GatewayIntentBits } from "discord.js"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { config } from "./src/config.js"; +import { MusicQueue } from "./src/music/queue.js"; +import { startWebServer } from "./src/web/server.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const client = new Client({ + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates], +}); + +client.commands = new Collection(); +const foldersPath = path.join(__dirname, "src/commands"); +const commandFolders = fs.readdirSync(foldersPath); + +for (const folder of commandFolders) { + if (folder.indexOf("[disabled]") === -1 || config.environnement === "test") { + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs + .readdirSync(commandsPath) + .filter((file) => file.endsWith(".js")); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + const commandFile = await import(filePath); + if ("data" in commandFile.command && "execute" in commandFile.command) { + client.commands.set(commandFile.command.data.name, commandFile); + } else { + console.log( + `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.` + ); + } + } + } +} + +const eventsPath = path.join(__dirname, "src/events"); +const eventFiles = fs + .readdirSync(eventsPath) + .filter((file) => file.endsWith(".js")); + +for (const file of eventFiles) { + const filePath = path.join(eventsPath, file); + const eventFile = await import(filePath); + if (eventFile.event.once) { + client.once(eventFile.event.name, (...args) => + eventFile.event.execute(...args) + ); + } else { + client.on(eventFile.event.name, (...args) => + eventFile.event.execute(...args) + ); + } +} + +client.login(config.token); + +const musicQueue = new MusicQueue(client); +startWebServer({ queue: musicQueue }); + +export { client, musicQueue }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2b75965 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2020 @@ +{ + "name": "palico-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "palico-bot", + "version": "1.0.0", + "license": "ISC", + "engines": { + "node": ">=20.6.0" + }, + "dependencies": { + "@discordjs/opus": "^0.10.0", + "@discordjs/voice": "^0.18.0", + "discord.js": "^14.18.0", + "express": "^5.1.0", + "libsodium-wrappers": "^0.7.15", + "multer": "^1.4.5-lts.1", + "uuid": "^11.0.3" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.0.tgz", + "integrity": "sha512-COK0uU6ZaJI+LA67H/rp8IbEkYwlZf3mAoBI5wtPh5G5cbEQGNhVpzINg2f/6+q/YipnNIKy6fJDg6kMUKUw4Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.1", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", + "integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.1" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/formatters/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/node-pre-gyp": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@discordjs/node-pre-gyp/-/node-pre-gyp-0.4.5.tgz", + "integrity": "sha512-YJOVVZ545x24mHzANfYoy0BJX5PDyeZlpiJjDkUBM/V/Ao7TFX9lcUvCN4nr0tbr5ubeaXxtEBILUrHtTphVeQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@discordjs/opus": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/opus/-/opus-0.10.0.tgz", + "integrity": "sha512-HHEnSNrSPmFEyndRdQBJN2YE6egyXS9JUnJWyP6jficK0Y+qKMEZXyYTgmzpjrxXP1exM/hKaNP7BRBUEWkU5w==", + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discordjs/node-pre-gyp": "^0.4.5", + "node-addon-api": "^8.1.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/voice": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@discordjs/voice/-/voice-0.18.0.tgz", + "integrity": "sha512-BvX6+VJE5/vhD9azV9vrZEt9hL1G+GlOdsQaVl5iv9n87fkXjf3cSwllhR3GdaUC8m6dqT8umXIWtn3yCu4afg==", + "license": "Apache-2.0", + "dependencies": { + "@types/ws": "^8.5.12", + "discord-api-types": "^0.37.103", + "prism-media": "^1.3.5", + "tslib": "^2.6.3", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.120", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.120.tgz", + "integrity": "sha512-7xpNK0EiWjjDFp2nAhHXezE4OUWm7s1zhc/UXXN6hnFFU8dfoPHgV0Hx0RPiCa3ILRpdeh152icc68DGCyXYIw==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.24.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.24.2.tgz", + "integrity": "sha512-VMEDbmguRdX/EeMaTsf9Mb0IQA90WdYF2cn4QDfslQFXgQ6LFtmlPn0FSotnS0kcFbFp+JBSIxtnF+bnAHG/hQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.1", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.31", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/discord-api-types": { + "version": "0.38.33", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.33.tgz", + "integrity": "sha512-oau1V7OzrNX8yNi+DfQpoLZCNCv7cTFmvPKwHfMrA/tewsO6iQKrMTzA7pa3iBSj0fED6NlklJ/1B/cC1kI08Q==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/libsodium": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", + "integrity": "sha512-sZwRknt/tUpE2AwzHq3jEyUU5uvIZHtSssktXq7owd++3CSgn8RGrv6UZJJBpP7+iBghBqe7Z06/2M31rI2NKw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.15.tgz", + "integrity": "sha512-E4anqJQwcfiC6+Yrl01C1m8p99wEhLmJSs0VQqST66SbQXXBoaJY0pF4BNjRYa/sOQAxx6lXAaAFIlx+15tXJQ==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.7.15" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/prism-media": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", + "integrity": "sha512-IQdl0Q01m4LrkN1EGIE9lphov5Hy7WWlH6ulf5QdGePLlPas9p2mhgddTEHrlaXYjjFToM1/rWuwF37VF4taaA==", + "license": "Apache-2.0", + "peerDependencies": { + "@discordjs/opus": ">=0.8.0 <1.0.0", + "ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0", + "node-opus": "^0.3.3", + "opusscript": "^0.0.8" + }, + "peerDependenciesMeta": { + "@discordjs/opus": { + "optional": true + }, + "ffmpeg-static": { + "optional": true + }, + "node-opus": { + "optional": true + }, + "opusscript": { + "optional": true + } + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "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/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/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" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..39454e3 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "palico-bot", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node --env-file=.env.local index.js", + "start:prod": "node --env-file=.env.production index.js", + "deploy": "node --env-file=.env.local deployCommands.js", + "deploy:prod": "node --env-file=.env.production deployCommands.js" + }, + "engines": { + "node": ">=20.6.0" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@discordjs/opus": "^0.10.0", + "@discordjs/voice": "^0.18.0", + "discord.js": "^14.18.0", + "express": "^5.1.0", + "libsodium-wrappers": "^0.7.15", + "multer": "^1.4.5-lts.1", + "uuid": "^11.0.3" + } +} diff --git a/public/img/palico-index.png b/public/img/palico-index.png new file mode 100644 index 0000000..dc389a7 Binary files /dev/null and b/public/img/palico-index.png differ diff --git a/public/img/palico-jukebox.png b/public/img/palico-jukebox.png new file mode 100644 index 0000000..3196cab Binary files /dev/null and b/public/img/palico-jukebox.png differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..9739f4c --- /dev/null +++ b/public/index.html @@ -0,0 +1,37 @@ + + + + + Palico Bot + + + + +
+
+ Palico Bot +
+ +
+

Palico Bot

+

Bienvenue sur la webapp

+

+ Lance de la musique sur le Discord et pilote la playlist du salon vocal. +

+
+
+ +
+
+ +
+
+ + diff --git a/public/jukebox.html b/public/jukebox.html new file mode 100644 index 0000000..359b84e --- /dev/null +++ b/public/jukebox.html @@ -0,0 +1,182 @@ + + + + + Palico Jukebox + + + + +
+
+ Palico Jukebox +
+
+

Le Jukebox du Palico Bot

+

Un peu de son pour le Discord !

+

+ Tu veux chill avec les autres sur un petit fond musical ?
+ T'inquietes pas, je peux m'occuper de ca !
+
+ Uploade des fichiers et gère la playlist depuis cette interface web. + Tout est synchronisé avec le salon vocal. +

+
+ + +
+
+ +
+
+

Authentification

+

+ Ca sera stocké sur ton navigateur si tu sauvegardes ! +

+
+
+
+ + +
+
+ + +
+
+
+ +
+
+
+
+
+ +
+ + +
+
+
+

Lecture en cours

+

+ État en temps réel du player Discord. Contrôle instantané. +

+
+ +
+
+ En attente de lecture... +
+
+ + + + +
+
+ +
+
+
+

File d'attente

+

+ Réorganise les morceaux, supprime-les ou ajoute ceux de la + bibliothèque. +

+
+
+
    +

    Aucun morceau en attente.

    +
    + +
    +

    Bibliothèque permanente

    +

    + Les fichiers conservés sont listés ici. Ajoute-les à la file ou + supprime-les. +

    +
      +

      + Aucun fichier permanent enregistré. +

      +
      + +
      +
      +
      +

      Uploader une musique

      +

      + Formats audio supportés par FFmpeg. Les fichiers temporaires sont + supprimés automatiquement après lecture. +

      +
      +
      Drag & drop supporté
      +
      +
      + + + + + + +
      + + +
      + + +
      +
      +
      + +
      + +
      + + + + diff --git a/public/scripts/common.js b/public/scripts/common.js new file mode 100644 index 0000000..241c5d4 --- /dev/null +++ b/public/scripts/common.js @@ -0,0 +1,118 @@ +const API_KEY_STORAGE_KEY = "palico-api-key"; +const DISPLAY_NAME_STORAGE_KEY = "palico-display-name"; + +export function getStoredApiKey() { + return localStorage.getItem(API_KEY_STORAGE_KEY) || ""; +} + +export function setStoredApiKey(value) { + localStorage.setItem(API_KEY_STORAGE_KEY, value || ""); +} + +export function getStoredDisplayName() { + return localStorage.getItem(DISPLAY_NAME_STORAGE_KEY) || ""; +} + +export function setStoredDisplayName(value) { + localStorage.setItem(DISPLAY_NAME_STORAGE_KEY, value || ""); +} + +export function createStatusManager(statusElement) { + let hideTimeout; + + function setStatus(message, type = "info", autoHide = true) { + if (!statusElement) return; + statusElement.textContent = message; + statusElement.className = `status status--${type}`; + statusElement.hidden = false; + + if (autoHide) { + clearTimeout(hideTimeout); + hideTimeout = setTimeout(() => { + statusElement.hidden = true; + }, 5000); + } + } + + function clearStatus() { + if (!statusElement) return; + statusElement.hidden = true; + statusElement.textContent = ""; + clearTimeout(hideTimeout); + } + + return { setStatus, clearStatus }; +} + +export function escapeHtml(text = "") { + return text + .toString() + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export function formatBytes(bytes = 0) { + if (!Number(bytes)) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + const index = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, index)).toFixed(1)} ${units[index]}`; +} + +export function formatDate(value) { + if (!value) return "?"; + try { + return new Date(value).toLocaleString(); + } catch (_) { + return value; + } +} + +export function createApiClient({ getApiKey, basePath = "" } = {}) { + const normalizedBase = (basePath || "").replace(/\/$/, ""); + + return async function apiFetch(path, options = {}) { + const url = path.startsWith("http") + ? path + : `${normalizedBase}${path.startsWith("/") ? "" : "/"}${path}`; + + const fetchOptions = { + method: options.method || "GET", + headers: new Headers(options.headers || {}), + }; + + const apiKey = typeof getApiKey === "function" ? getApiKey() : null; + if (apiKey) { + fetchOptions.headers.set("Authorization", `Bearer ${apiKey}`); + } + + if (options.body instanceof FormData) { + fetchOptions.body = options.body; + } else if (options.body) { + if (!fetchOptions.headers.has("Content-Type")) { + fetchOptions.headers.set("Content-Type", "application/json"); + } + fetchOptions.body = + typeof options.body === "string" ? options.body : JSON.stringify(options.body); + } + + const response = await fetch(url, fetchOptions); + const contentType = response.headers.get("content-type") || ""; + let payload = null; + + if (contentType.includes("application/json")) { + payload = await response.json(); + } else { + payload = await response.text(); + } + + if (!response.ok) { + const errorMessage = payload?.error || payload?.message || response.statusText; + throw new Error(errorMessage); + } + + return payload; + }; +} diff --git a/public/scripts/jukebox.js b/public/scripts/jukebox.js new file mode 100644 index 0000000..10c64d8 --- /dev/null +++ b/public/scripts/jukebox.js @@ -0,0 +1,319 @@ +import { + createApiClient, + createStatusManager, + escapeHtml, + getStoredApiKey, + getStoredDisplayName, + formatBytes, + formatDate, + setStoredApiKey, + setStoredDisplayName, +} from "./common.js"; + +const state = { + apiKey: getStoredApiKey(), + displayName: getStoredDisplayName(), + queue: [], + current: null, + library: [], + status: "idle", +}; + +const elements = { + status: document.getElementById("status"), + apiKeyInput: document.getElementById("apiKey"), + displayNameInput: document.getElementById("displayName"), + settingsForm: document.getElementById("settingsForm"), + uploadForm: document.getElementById("uploadForm"), + fileInput: document.getElementById("musicFile"), + notesInput: document.getElementById("uploadNotes"), + permanentCheckbox: document.getElementById("permanentFile"), + enqueueCheckbox: document.getElementById("enqueueFile"), + currentTrack: document.getElementById("currentTrack"), + queueList: document.getElementById("queueList"), + queueEmpty: document.getElementById("queueEmpty"), + libraryList: document.getElementById("libraryList"), + libraryEmpty: document.getElementById("libraryEmpty"), + refreshBtn: document.getElementById("refreshBtn"), + pauseResumeBtn: document.getElementById("pauseResumeBtn"), + playerButtons: document.querySelectorAll("[data-player-action]"), + heroUploadBtn: document.getElementById("heroUploadBtn"), + heroQueueBtn: document.getElementById("heroQueueBtn"), + uploadSection: document.getElementById("uploadSection"), + queueSection: document.getElementById("queueSection"), +}; + +let pollIntervalId = null; + +const { setStatus, clearStatus } = createStatusManager(elements.status); +const apiFetch = createApiClient({ + basePath: "/api", + getApiKey: () => state.apiKey, +}); + +async function refreshAll({ silent = false } = {}) { + if (!silent) { + clearStatus(); + } + + try { + const [queueData, libraryData] = await Promise.all([ + apiFetch("/queue"), + apiFetch("/library"), + ]); + + state.current = queueData.current; + state.queue = queueData.upcoming || []; + state.library = libraryData.tracks || []; + state.status = queueData.status || "idle"; + + renderCurrentTrack(); + renderQueue(); + renderLibrary(); + updatePauseResumeButton(); + } catch (error) { + setStatus(`Erreur: ${error.message}`, "error", false); + } +} + +function renderCurrentTrack() { + if (!elements.currentTrack) return; + if (!state.current) { + elements.currentTrack.classList.add("player-current--empty"); + elements.currentTrack.innerHTML = "En attente de lecture..."; + return; + } + + elements.currentTrack.classList.remove("player-current--empty"); + const track = state.current; + const permanence = track.permanent ? "Permanent" : "Temporaire"; + elements.currentTrack.innerHTML = ` +
      ${escapeHtml(track.title || track.originalName)}
      +
      + ${permanence} • ${formatBytes(track.size)} • Ajouté par ${escapeHtml( + track.uploader || "inconnu" + )} +
      +
      Commencé à ${formatDate(track.createdAt)}
      + `; +} + +function renderQueue() { + if (!elements.queueList) return; + elements.queueList.innerHTML = ""; + + if (!state.queue || state.queue.length === 0) { + elements.queueEmpty.style.display = "block"; + return; + } + + elements.queueEmpty.style.display = "none"; + + state.queue.forEach((track, index) => { + const li = document.createElement("li"); + li.className = "media-item media-item--queue"; + const permanence = track.permanent ? "Permanent" : "Temporaire"; + li.innerHTML = ` +
      +
      ${escapeHtml(track.title || track.originalName)}
      +
      + ${permanence} • ${formatBytes(track.size)} • Ajouté par ${escapeHtml( + track.uploader || "inconnu" + )} +
      +
      +
      + + + +
      + `; + + li.querySelectorAll("button").forEach((btn) => { + btn.addEventListener("click", (event) => { + const action = event.currentTarget.dataset.action; + const trackId = event.currentTarget.dataset.track; + handleQueueAction(action, trackId, index); + }); + }); + + elements.queueList.appendChild(li); + }); +} + +function renderLibrary() { + if (!elements.libraryList) return; + elements.libraryList.innerHTML = ""; + + if (!state.library || state.library.length === 0) { + elements.libraryEmpty.style.display = "block"; + return; + } + + elements.libraryEmpty.style.display = "none"; + + state.library.forEach((track) => { + const li = document.createElement("li"); + li.className = "media-item media-item--library"; + li.innerHTML = ` +
      +
      ${escapeHtml(track.originalName)}
      +
      + ${formatBytes(track.size)} • Ajouté le ${formatDate(track.createdAt)} +
      +
      +
      + + +
      + `; + + li.querySelectorAll("button").forEach((btn) => { + btn.addEventListener("click", (event) => { + const action = event.currentTarget.dataset.libraryAction; + const libraryId = event.currentTarget.dataset.libraryId; + handleLibraryAction(action, libraryId); + }); + }); + + elements.libraryList.appendChild(li); + }); +} + +function updatePauseResumeButton() { + if (!elements.pauseResumeBtn) return; + const normalizedStatus = + typeof state.status === "string" ? state.status.toLowerCase() : "idle"; + const isPaused = normalizedStatus === "paused"; + const isPlaying = normalizedStatus === "playing"; + + if (isPaused) { + elements.pauseResumeBtn.textContent = "Reprendre"; + elements.pauseResumeBtn.dataset.playerAction = "resume"; + } else { + elements.pauseResumeBtn.textContent = "Pause"; + elements.pauseResumeBtn.dataset.playerAction = "pause"; + } + + const actionable = isPaused || isPlaying; + elements.pauseResumeBtn.disabled = !actionable; +} + +async function handleQueueAction(action, trackId, index) { + try { + if (action === "remove") { + await apiFetch(`/queue/${trackId}`, { method: "DELETE" }); + } else if (action === "up" || action === "down") { + const delta = action === "up" ? -1 : 1; + const newPosition = Math.max(0, Math.min(index + delta, state.queue.length - 1)); + if (newPosition === index) return; + await apiFetch(`/queue/${trackId}/move`, { + method: "PATCH", + body: { position: newPosition }, + }); + } + await refreshAll({ silent: true }); + } catch (error) { + setStatus(`Impossible de mettre à jour la file: ${error.message}`, "error"); + } +} + +async function handleLibraryAction(action, libraryId) { + try { + if (action === "enqueue") { + await apiFetch(`/library/${libraryId}/enqueue`, { + method: "POST", + body: { requestedBy: state.displayName || "web-ui" }, + }); + } else if (action === "delete") { + if (!confirm("Supprimer ce fichier permanent ?")) { + return; + } + await apiFetch(`/library/${libraryId}`, { method: "DELETE" }); + } + await refreshAll({ silent: true }); + } catch (error) { + setStatus(`Action bibliothèque impossible: ${error.message}`, "error"); + } +} + +function startPolling() { + clearInterval(pollIntervalId); + pollIntervalId = setInterval(() => refreshAll({ silent: true }), 5000); +} + +function bindEvents() { + elements.settingsForm?.addEventListener("submit", (event) => { + event.preventDefault(); + state.apiKey = elements.apiKeyInput.value.trim(); + state.displayName = elements.displayNameInput.value.trim(); + setStoredApiKey(state.apiKey); + setStoredDisplayName(state.displayName); + setStatus("Paramètres sauvegardés", "success"); + refreshAll({ silent: true }); + }); + + elements.uploadForm?.addEventListener("submit", async (event) => { + event.preventDefault(); + const file = elements.fileInput.files[0]; + if (!file) { + setStatus("Merci de sélectionner un fichier audio", "error"); + return; + } + + try { + const data = new FormData(); + data.append("file", file); + data.append("permanent", elements.permanentCheckbox.checked ? "true" : "false"); + data.append("enqueue", elements.enqueueCheckbox.checked ? "true" : "false"); + if (state.displayName) { + data.append("uploader", state.displayName); + } + if (elements.notesInput.value) { + data.append("notes", elements.notesInput.value.trim()); + } + + setStatus("Upload en cours...", "info", false); + await apiFetch("/uploads", { method: "POST", body: data }); + elements.uploadForm.reset(); + elements.enqueueCheckbox.checked = true; + setStatus("Fichier envoyé avec succès", "success"); + await refreshAll({ silent: true }); + } catch (error) { + setStatus(`Échec de l'upload: ${error.message}`, "error"); + } + }); + + elements.refreshBtn?.addEventListener("click", () => refreshAll()); + elements.heroUploadBtn?.addEventListener("click", () => + elements.uploadSection?.scrollIntoView({ behavior: "smooth" }) + ); + elements.heroQueueBtn?.addEventListener("click", () => + elements.queueSection?.scrollIntoView({ behavior: "smooth" }) + ); + + elements.playerButtons?.forEach((button) => { + button.addEventListener("click", async () => { + const action = button.dataset.playerAction; + try { + await apiFetch(`/player/${action}`, { method: "POST" }); + await refreshAll({ silent: true }); + } catch (error) { + setStatus(`Action player impossible: ${error.message}`, "error"); + } + }); + }); +} + +function initialize() { + elements.apiKeyInput.value = state.apiKey; + elements.displayNameInput.value = state.displayName; + bindEvents(); + updatePauseResumeButton(); + refreshAll(); + startPolling(); +} + +initialize(); diff --git a/public/style/styles.css b/public/style/styles.css new file mode 100644 index 0000000..6a2e9d6 --- /dev/null +++ b/public/style/styles.css @@ -0,0 +1,415 @@ +: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; +} + +.hero--home { + flex-direction: row; + align-items: center; + text-align: left; + gap: 2.75rem; +} + +.hero--home .hero__visual img { + max-width: 320px; +} + +.hero__content { + flex: 1; + min-width: 280px; +} + +.hero__content h1 { + font-size: 2.2rem; + line-height: 1.1; + margin-top: 1rem; + margin-bottom: 1rem; +} + +.hero__content p { + color: var(--muted); + font-size: 1.05rem; + max-width: 500px; +} + +.hero__actions { + display: flex; + gap: 1rem; + margin-top: 1.5rem; +} + +.hero__actions--centered { + justify-content: center; + margin: 2rem auto 3rem; +} + +.hero__visual { + min-width: 200px; + display: flex; + justify-content: center; +} + +.hero__visual img { + max-width: 200px; + width: 100%; + filter: drop-shadow(0 25px 45px rgba(0, 0, 0, 0.45)); +} + +.hero__visual--large img { + max-width: 270px; +} + +.hero__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); +} + +.panel--glass { + background: var(--bg-glass); + box-shadow: 0 20px 38px rgba(2, 0, 8, 0.45); + backdrop-filter: blur(18px); +} + +.panel__subtitle { + color: var(--muted); + margin-top: 0.35rem; + margin-bottom: 1.5rem; +} + +.panel__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); +} + +.form-checkbox__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; +} + +.btn:hover { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(178, 89, 255, 0.35); +} + +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; + box-shadow: none; + transform: none; +} + +.btn--secondary { + background: rgba(255, 255, 255, 0.15); + color: #fff; +} + +.btn--danger { + background: linear-gradient(120deg, #ff4d6d, #ff89a5); + color: #fff; +} + +.btn--ghost { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.35); + color: #fff; +} + +.btn--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; +} + +.grid--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; +} + +.status--info { + background: rgba(178, 89, 255, 0.2); + color: var(--accent); +} + +.status--error { + background: rgba(255, 77, 109, 0.15); + color: var(--danger); +} + +.status--success { + background: rgba(89, 255, 167, 0.15); + color: var(--success); +} + +.list { + list-style: none; + padding: 0; + margin: 0; +} + +.list--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; +} + +.media-item__info { + flex: 1; +} + +.media-item__title { + font-weight: 600; + font-size: 1rem; +} + +.media-item__meta { + color: var(--muted); + font-size: 0.9rem; +} + +.media-item__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); +} + +.player-current--empty { + color: var(--muted); + text-align: center; +} + +.feature-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); +} + +.feature-grid__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; +} + +.feature-grid__card:hover { + transform: translateY(-3px); + box-shadow: 0 15px 30px rgba(2, 0, 8, 0.45); +} + +.feature-grid__card .pill { + margin-bottom: 0.5rem; +} + +@media (max-width: 900px) { + .hero, + .hero--home { + flex-direction: column; + text-align: center; + } + + .hero__content p { + margin: 0 auto; + } + + .hero__actions { + justify-content: center; + } + + .panel__header { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 600px) { + .btn, + input[type="text"], + input[type="file"], + select, + textarea { + width: 100%; + } + + .control-group { + flex-direction: column; + } +} diff --git a/public/style/styles.scss b/public/style/styles.scss new file mode 100644 index 0000000..b8e2f7f --- /dev/null +++ b/public/style/styles.scss @@ -0,0 +1,417 @@ +:root { + color-scheme: dark; + --bg: #06030d; + --bg-card: rgba(22, 14, 36, 0.9); + --bg-glass: rgba(42, 25, 66, 0.65); + --border: rgba(255, 255, 255, 0.08); + --accent: #b259ff; + --accent-strong: #d66bff; + --muted: #cbc4d5; + --muted-dark: #8c7ea7; + --danger: #ff4d6d; + --success: #59ffa7; + --gradient: radial-gradient(circle at 20% 20%, #2d1a49, transparent 45%), + radial-gradient(circle at 80% 0%, #551e8c, transparent 40%), + radial-gradient(circle at 50% 80%, #150b28, transparent 30%); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + font-family: "Inter", "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--gradient), #050109; + color: #f7f1ff; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem 4rem; +} + +h1, +h2, +h3, +h4 { + margin: 0; +} + +p { + margin-top: 0; +} + +.text { + &--muted { + color: var(--muted); + text-align: center; + } +} + +.hero { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 2rem; + padding: 3rem 1.5rem; + max-width: 1200px; + margin: 0 auto; + + &--home { + flex-direction: row; + align-items: center; + text-align: left; + gap: 2.75rem; + + .hero__visual img { + max-width: 320px; + } + } + + &__content { + flex: 1; + min-width: 280px; + + h1 { + font-size: 2.2rem; + line-height: 1.1; + margin-top: 1rem; + margin-bottom: 1rem; + } + + p { + color: var(--muted); + font-size: 1.05rem; + max-width: 500px; + } + } + + &__actions { + display: flex; + gap: 1rem; + margin-top: 1.5rem; + + &--centered { + justify-content: center; + margin: 2rem auto 3rem; + } + } + + &__visual { + min-width: 200px; + display: flex; + justify-content: center; + + img { + max-width: 200px; + width: 100%; + filter: drop-shadow(0 25px 45px rgba(0, 0, 0, 0.45)); + } + + &--large img { + max-width: 270px; + } + } + + &__auth { + max-width: 300px; + } +} + +.pill { + display: inline-flex; + align-items: center; + gap: 0.45rem; + font-size: 0.9rem; + padding: 0.35rem 0.85rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + color: var(--muted); + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.panel { + border-radius: 18px; + padding: 1.75rem; + margin-bottom: 1.75rem; + border: 1px solid var(--border); + + &--glass { + background: var(--bg-glass); + box-shadow: 0 20px 38px rgba(2, 0, 8, 0.45); + backdrop-filter: blur(18px); + } + + &__subtitle { + color: var(--muted); + margin-top: 0.35rem; + margin-bottom: 1.5rem; + } + + &__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.25rem; + } +} + +.badge { + padding: 0.35rem 0.9rem; + background: rgba(178, 89, 255, 0.2); + color: var(--accent); + border-radius: 12px; + font-size: 0.85rem; +} + +label { + display: block; + font-size: 0.95rem; + color: var(--muted); + margin-bottom: 0.35rem; +} + +input[type="text"], +input[type="file"], +select, +textarea { + width: 100%; + padding: 0.7rem 0.9rem; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.05); + color: #fff; + font-size: 1rem; +} + +input[type="file"] { + padding: 0.5rem; +} + +input:focus, +textarea:focus, +select:focus { + outline: 2px solid rgba(178, 89, 255, 0.5); + border-color: rgba(178, 89, 255, 0.6); +} + +.form-checkbox { + display: flex; + align-items: flex-start; + gap: 0.5rem; + color: var(--muted); + + &__input { + margin-top: 0.25rem; + } +} + +.btn { + border: none; + border-radius: 999px; + padding: 0.65rem 1.55rem; + font-weight: 600; + cursor: pointer; + background: linear-gradient(120deg, var(--accent), var(--accent-strong)); + color: #08010f; + transition: transform 0.15s ease, box-shadow 0.15s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(178, 89, 255, 0.35); + } + + &:disabled { + opacity: 0.55; + cursor: not-allowed; + box-shadow: none; + transform: none; + } + + &--secondary { + background: rgba(255, 255, 255, 0.15); + color: #fff; + } + + &--danger { + background: linear-gradient(120deg, #ff4d6d, #ff89a5); + color: #fff; + } + + &--ghost { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.35); + color: #fff; + } + + &--small { + padding: 0.4rem 1rem; + font-size: 0.9rem; + } +} + +.control-group { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.25rem; +} + +.grid { + display: grid; + gap: 1rem; + + &--two { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + } +} + +.u-mt-1 { + margin-top: 1rem; +} + +.form-actions { + margin-top: 1.25rem; + display: flex; + justify-content: flex-end; +} + +.status { + padding: 0.85rem 1.25rem; + border-radius: 999px; + font-size: 0.95rem; + margin: 1rem 0 2rem; + max-width: 600px; + + &--info { + background: rgba(178, 89, 255, 0.2); + color: var(--accent); + } + + &--error { + background: rgba(255, 77, 109, 0.15); + color: var(--danger); + } + + &--success { + background: rgba(89, 255, 167, 0.15); + color: var(--success); + } +} + +.list { + list-style: none; + padding: 0; + margin: 0; + + &--stack > li { + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } +} + +.media-item { + display: flex; + gap: 1rem; + justify-content: space-between; + align-items: center; + padding: 0.9rem 0; + + &__info { + flex: 1; + } + + &__title { + font-weight: 600; + font-size: 1rem; + } + + &__meta { + color: var(--muted); + font-size: 0.9rem; + } + + &__actions { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + } +} + +.player-current { + background: rgba(0, 0, 0, 0.25); + border-radius: 16px; + padding: 1.2rem; + border: 1px solid rgba(255, 255, 255, 0.08); + + &--empty { + color: var(--muted); + text-align: center; + } +} + +.feature-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + + &__card { + text-decoration: none; + color: inherit; + display: block; + border: 1px solid var(--border); + border-radius: 18px; + padding: 1.25rem 1.5rem; + background: var(--bg-card); + transition: transform 0.15s ease, box-shadow 0.15s ease; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 15px 30px rgba(2, 0, 8, 0.45); + } + + .pill { + margin-bottom: 0.5rem; + } + } +} + +@media (max-width: 900px) { + .hero, + .hero--home { + flex-direction: column; + text-align: center; + } + + .hero__content p { + margin: 0 auto; + } + + .hero__actions { + justify-content: center; + } + + .panel__header { + flex-direction: column; + align-items: flex-start; + } +} + +@media (max-width: 600px) { + .btn, + input[type="text"], + input[type="file"], + select, + textarea { + width: 100%; + } + + .control-group { + flex-direction: column; + } +} diff --git a/src/commands/game/dice.js b/src/commands/game/dice.js new file mode 100644 index 0000000..0bf8d03 --- /dev/null +++ b/src/commands/game/dice.js @@ -0,0 +1,71 @@ +import { SlashCommandBuilder, subtext } from "discord.js"; +import { setTimeout } from "node:timers/promises"; +import { getRandomInt } from "../../features/utilities.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("dice") + .setDescription("Lance un ou plusieurs dés") + .addIntegerOption((option) => + option + .setName("number") + .setDescription("Nombre de dés a lancer") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("faces") + .setDescription("Nombre de faces sur le dé") + .setRequired(true) + ), + async execute(interaction) { + const number = interaction.options.getInteger("number"); + const faces = interaction.options.getInteger("faces"); + var sum = 0; + var message = "Alors du coup... 🥁\n"; + var tails = 0; + var heads = 0; + + if (faces === 1 || faces === 0) { + await interaction.reply({ + content: "Ouais ouais, bien sur. Allez, bisous !", + ephemeral: true, + }); + return; + } + + if (faces === 2) { + message = message + subtext(`Pile = 1 et Face = 2\n`); + } + + await interaction.reply(message); + + for (let i = 0; i < number; i++) { + await setTimeout(1_500); + let random = getRandomInt(faces) + 1; + random === 1 ? tails++ : heads++; + sum = sum + random; + + message = message + `[ ${random} ] ${i === number - 1 ? "\n" : " "}`; + interaction.editReply(message); + } + + if (number > 1 && faces > 2) { + await setTimeout(1_000); + message = message + subtext(`Soit au total : ${sum}`); + } + + if (number > 1 && faces === 2) { + await setTimeout(1_000); + message = + message + + subtext( + `Soit au total : ${sum}, ou bien ${tails} piles et ${heads} faces \n` + ); + } + + interaction.editReply(message); + }, +}; + +export { command }; diff --git a/src/commands/game/maketeam.js b/src/commands/game/maketeam.js new file mode 100644 index 0000000..90f4ce4 --- /dev/null +++ b/src/commands/game/maketeam.js @@ -0,0 +1,44 @@ +import { SlashCommandBuilder } from "discord.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("maketeam") + .setDescription("Crée des equipes au hasard") + .addIntegerOption((option) => + option + .setName("number") + .setDescription("Nombre d'équipe") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("players") + .setDescription("Liste des joueurs, séparés par un espace") + .setRequired(true) + ), + async execute(interaction) { + const teamNumber = interaction.options.getInteger("number"); + const players = interaction.options.getString("players"); + const playersArray = players.split(" "); + + const teams = Array.from({ length: teamNumber }, () => []); + const shuffledPlayers = [...playersArray].sort(() => Math.random() - 0.5); + + shuffledPlayers.forEach((player, index) => { + const teamIndex = index % teamNumber; + teams[teamIndex].push(player); + }); + + let message = ""; + let teamList = ""; + + teams.forEach((team, index) => { + teamList = `${teamList}\n${index + 1}. ${team.join(", ")}`; + }); + + message = `Yop ! Voici les équipes proposés :\n${teamList}`; + await interaction.reply(message); + }, +}; + +export { command }; diff --git a/src/commands/utility/deletemsgs.js b/src/commands/utility/deletemsgs.js new file mode 100644 index 0000000..c9403d5 --- /dev/null +++ b/src/commands/utility/deletemsgs.js @@ -0,0 +1,31 @@ +import { SlashCommandBuilder } from "discord.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("deletemsgs") + .setDescription("Supprime tous les messages, sauf ceux épinglés"), + async execute(interaction) { + let textChannel = interaction.client.channels.cache.get( + interaction.channelId + ); + + textChannel.messages + .fetch({ limit: 100 }) + .then((fetched) => { + const notPinned = fetched.filter((fetchedMsg) => !fetchedMsg.pinned); + textChannel.bulkDelete(notPinned, true); + console.log( + `[${new Date().toLocaleString()}] Command : Delete Messages used` + ); + }) + .catch(console.error); + + interaction.reply("Et voila, c'est tout propre ! 🐱").then(() => { + setTimeout(() => { + interaction.deleteReply(); + }, 7000); + }); + }, +}; + +export { command }; diff --git a/src/commands/utility/listcommand.js b/src/commands/utility/listcommand.js new file mode 100644 index 0000000..fb0fc3d --- /dev/null +++ b/src/commands/utility/listcommand.js @@ -0,0 +1,27 @@ +import { SlashCommandBuilder } from "discord.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("listcommand") + .setDescription("Affiche la liste des commandes disponibles"), + async execute(interaction) { + let message = `Voila la liste des commandes que tu peux utiliser : + __Categorie Utililaires__ : + \`/deletemsgs\`: Supprime les 100 derniers messages non épinglés + + __Categorie Vocal__ : + \`/muteall\`: Rends tout le monde muet + \`/unmuteall\`: Rends la parole à tout le monde + \`/deafall\`: Rends tout le monde sourd + \`/undeafall\`: Rends l'ouïe à tout le monde + + __Categorie Jeux__ : + \`/dice\`: Lance un ou plusieurs dés avec un nombre de faces choisi + \`/maketeam\`: Crée un certain nombre d'équipes en renseignant des joueurs (séparés par un espace) + `; + + await interaction.reply({ content: message, ephemeral: true }); + }, +}; + +export { command }; diff --git a/src/commands/utility/ping.js b/src/commands/utility/ping.js new file mode 100644 index 0000000..50fc710 --- /dev/null +++ b/src/commands/utility/ping.js @@ -0,0 +1,13 @@ +import { SlashCommandBuilder } from "discord.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("ping") + .setDescription("Reponds avec Pong"), + async execute(interaction) { + console.log(interaction); + await interaction.reply({ content: "Euh... Pong ! 🐱", ephemeral: true }); + }, +}; + +export { command }; diff --git a/src/commands/vocal/deafall.js b/src/commands/vocal/deafall.js new file mode 100644 index 0000000..7c8bcbe --- /dev/null +++ b/src/commands/vocal/deafall.js @@ -0,0 +1,14 @@ +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("deafall") + .setDescription("Deaf tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "deaf", "all"); + await interaction.reply("On n'entends plus rien ! 🐱"); + }, +}; + +export { command }; diff --git a/src/commands/vocal/muteall.js b/src/commands/vocal/muteall.js new file mode 100644 index 0000000..33a6005 --- /dev/null +++ b/src/commands/vocal/muteall.js @@ -0,0 +1,14 @@ +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("muteall") + .setDescription("Mute tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "mute", "all"); + await interaction.reply("Voila, tout le monde est mioute ! 🐱"); + }, +}; + +export { command }; diff --git a/src/commands/vocal/undeafall.js b/src/commands/vocal/undeafall.js new file mode 100644 index 0000000..fa60861 --- /dev/null +++ b/src/commands/vocal/undeafall.js @@ -0,0 +1,14 @@ +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("undeafall") + .setDescription("Undeaf tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "undeaf", "all"); + await interaction.reply("Voila, les oreilles sont réparées ! 🐱"); + }, +}; + +export { command }; diff --git a/src/commands/vocal/unmuteall.js b/src/commands/vocal/unmuteall.js new file mode 100644 index 0000000..ba9f4ed --- /dev/null +++ b/src/commands/vocal/unmuteall.js @@ -0,0 +1,14 @@ +import { SlashCommandBuilder } from "discord.js"; +import { changeVoiceStatus } from "../../features/utilities.js"; + +const command = { + data: new SlashCommandBuilder() + .setName("unmuteall") + .setDescription("Unmute tout le monde dans le lounge"), + async execute(interaction) { + changeVoiceStatus(interaction.client, "unmute", "all"); + await interaction.reply("Et.... unmioute ! 🐱"); + }, +}; + +export { command }; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..6ddb3af --- /dev/null +++ b/src/config.js @@ -0,0 +1,49 @@ +const requiredVariables = [ + "NODE_ENV", + "LOCATION", + "BOT_OWNER", + "DISCORD_GUILD_ID", + "SHINUWA_USER_ID", + "INVITE_LINK", + "WEB_SERVER_PORT", + "WEB_API_KEY", + "DISCORD_APPLICATION_ID", + "DISCORD_TOKEN", + "VOICE_CHANNEL_ID", + "TEXT_CHANNEL_ID", + "GAME_SERVERS_MESSAGE_ID", +]; + +const missingVariables = requiredVariables.filter( + (name) => !process.env[name]?.trim() +); + +if (missingVariables.length > 0) { + throw new Error( + `Variables d'environnement manquantes : ${missingVariables.join(", ")}` + ); +} + +const webServerPort = Number(process.env.WEB_SERVER_PORT); + +if (!Number.isInteger(webServerPort) || webServerPort <= 0) { + throw new Error("WEB_SERVER_PORT doit être un entier positif"); +} + +const config = Object.freeze({ + environnement: process.env.NODE_ENV, + location: process.env.LOCATION, + botOwner: process.env.BOT_OWNER, + discordGuildId: process.env.DISCORD_GUILD_ID, + shinuwaUserId: process.env.SHINUWA_USER_ID, + inviteLink: process.env.INVITE_LINK, + webServerPort, + webApiKey: process.env.WEB_API_KEY, + applicationId: process.env.DISCORD_APPLICATION_ID, + token: process.env.DISCORD_TOKEN, + voiceChannelId: process.env.VOICE_CHANNEL_ID, + textChannelId: process.env.TEXT_CHANNEL_ID, + gameServersMessageId: process.env.GAME_SERVERS_MESSAGE_ID, +}); + +export { config }; diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js new file mode 100644 index 0000000..f75b69f --- /dev/null +++ b/src/events/interactionCreate.js @@ -0,0 +1,38 @@ +import { Events } from "discord.js"; + +const event = { + name: Events.InteractionCreate, + async execute(interaction) { + if (!interaction.isChatInputCommand()) return; + + const command = interaction.client.commands.get( + interaction.commandName + ).command; + + if (!command) { + console.error( + `No command matching ${interaction.commandName} was found.` + ); + return; + } + + try { + await command.execute(interaction); + } catch (error) { + console.error(error); + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } else { + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + } + }, +}; + +export { event }; diff --git a/src/events/ready.js b/src/events/ready.js new file mode 100644 index 0000000..7a0da5d --- /dev/null +++ b/src/events/ready.js @@ -0,0 +1,26 @@ +import { Events } from "discord.js"; +import { config } from "../config.js"; +import { showGameServers } from "../features/showGameServers.js"; +import { sendMessage } from "../features/sendMessage.js"; +import { isConnectedToVoiceChannel } from "../features/utilities.js"; + +const event = { + name: Events.ClientReady, + once: true, + execute(client) { + console.log( + `[${new Date().toLocaleString()}] Logged in as ${client.user.tag}` + ); + + showGameServers( + client, + config.textChannelId, + config.gameServersMessageId, + config.shinuwaUserId + ); + + //sendMessage(client, 'jukebox') + }, +}; + +export { event }; diff --git a/src/features/sendMessage.js b/src/features/sendMessage.js new file mode 100644 index 0000000..0e977bf --- /dev/null +++ b/src/features/sendMessage.js @@ -0,0 +1,59 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + channelMention, + codeBlock, +} from "discord.js"; +import { config } from "../config.js"; + +function sendMessage(client, type) { + const channel = client.channels.cache.get(config.textChannelId); + var message = { content: "Message réservé" }; + + if (type && type === "invitation") { + message.content = inviteMessage(); + } else if (type && type === "jukebox") { + message = jukeboxMessage(); + } + + channel.send(message); +} + +function inviteMessage() { + var message = + "Pour inviter quelqu'un, il faut utiliser cette invitation temporaire !\n"; + + message = message + codeBlock(config.inviteLink); + + message = + message + + "*Pour une invitation permanente, il suffit d'attribuer le role*" + + " ***Hangaround*** " + + "*à la personne concernée.*\n\n" + + "​"; + + return message; +} + +function jukeboxMessage() { + const description = + "Yo ! 😺\n\n" + + `J'ai installé un jukebox dans ${channelMention(config.voiceChannelId)} !\n` + + "Tu peux le contrôler facilement via une petite interface ! 😽.\n\n" + + "Clique sur le bouton ci-dessous pour ouvrir le tableau de bord.\n\n"; + + const buttonRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Ouvrir le Jukebox 🎵") + .setStyle(ButtonStyle.Link) + .setURL("https://palico-bot.shinuwa.fr/jukebox/") + ); + + return { + content: description, + components: [buttonRow], + }; +} + +export { sendMessage, inviteMessage, jukeboxMessage }; diff --git a/src/features/showGameServers.js b/src/features/showGameServers.js new file mode 100644 index 0000000..43478b1 --- /dev/null +++ b/src/features/showGameServers.js @@ -0,0 +1,64 @@ +import { config } from "../config.js"; +import { getJSONData } from "./utilities.js"; + +var currentStatus = []; + +function checkServersStatus(client) { + const data = getJSONData("src/static/gameservers-list.json"); + const newStatus = data.servers.map((server) => server.active); + + if (JSON.stringify(newStatus) === JSON.stringify(currentStatus)) { + return; + } + + updateMessage(client, data); + currentStatus = newStatus; + return true; +} + +function updateMessage(client, data) { + var newMessage = `Hello 😺 ! Voila les serveurs de jeux qu'on a chez nous :\n\n`; + + data.servers.forEach((server) => { + newMessage = + newMessage + + "```ansi\n" + + `${server.game} : ` + + `${server.active ? "Online" : "Offline"}\n` + + `${server.address}\n` + + "```"; + }); + + newMessage = + newMessage + + "\nAh d'ailleurs ! Si le serveur est sécurisé, le mot de passe c'est surement **0117**."; + newMessage = + newMessage + + `\nPour mettre en ligne un serveur, faut voir avec <@${config.shinuwaUserId}> , c'est lui qui gère ca !\n`; + + const channel = client.channels.cache.get(config.textChannelId); + + channel.messages + .fetch(config.gameServersMessageId) + .then((message) => { + message + .edit(newMessage) + .then( + console.log( + `[${new Date().toLocaleString()}] showGameServers: Game servers message updated` + ) + ) + .catch(console.error); + }) + .catch(console.error); +} + +function showGameServers(client) { + const data = getJSONData("src/static/gameservers-list.json"); + currentStatus = data.servers.map((server) => server.active); + + updateMessage(client, data); + setInterval(checkServersStatus, 120000, client); +} + +export { showGameServers }; diff --git a/src/features/utilities.js b/src/features/utilities.js new file mode 100644 index 0000000..50fca3f --- /dev/null +++ b/src/features/utilities.js @@ -0,0 +1,74 @@ +import fs from "fs"; +import { config } from "../config.js"; + +function changeVoiceStatus(client, status, members) { + const changeStatus = (type, value, members) => { + switch (type) { + case "mute": + for (let member of members) { + member[1].voice.setMute(value); + } + break; + case "deaf": + for (let member of members) { + member[1].voice.setDeaf(value); + } + break; + } + }; + + var voiceChannel = client.channels.cache.get(config.voiceChannelId); + var connectedMembers = voiceChannel.members; + + if (members == "all") { + members = connectedMembers; + } + + switch (status) { + case "mute": + changeStatus("mute", true, members); + break; + case "unmute": + changeStatus("mute", false, members); + break; + case "deaf": + changeStatus("deaf", true, members); + break; + case "undeaf": + changeStatus("deaf", false, members); + break; + } +} + +function getJSONData(file) { + const jsonFile = fs.readFileSync(file); + const data = JSON.parse(jsonFile); + + return data; +} + +function getRandomInt(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 { + changeVoiceStatus, + getJSONData, + getRandomInt, + isConnectedToVoiceChannel, +}; diff --git a/src/music/queue.js b/src/music/queue.js new file mode 100644 index 0000000..1bbc880 --- /dev/null +++ b/src/music/queue.js @@ -0,0 +1,370 @@ +import fs from "fs"; +import { EventEmitter } from "events"; +import { + AudioPlayerStatus, + NoSubscriberBehavior, + VoiceConnectionStatus, + createAudioPlayer, + createAudioResource, + entersState, + joinVoiceChannel, +} from "@discordjs/voice"; +import { Events } from "discord.js"; +import { config } from "../config.js"; +import { + deleteTrackFile, + handleTrackCompletion, +} from "../web/storage/storageManager.js"; + +const AUTO_DISCONNECT_DELAY = 5 * 60 * 1000; + +class MusicQueue extends EventEmitter { + constructor(client) { + super(); + this.client = client; + this.queue = []; + this.currentTrack = null; + this.connection = null; + this.playbackStatus = AudioPlayerStatus.Idle; + this.idleDisconnectTimeout = null; + this.aloneDisconnectTimeout = null; + this.suspendedForAlone = false; + this.audioPlayer = createAudioPlayer({ + behaviors: { + noSubscriber: NoSubscriberBehavior.Play, + }, + }); + this.voiceStateListener = (oldState, newState) => { + if ( + oldState.channelId === config.voiceChannelId || + newState.channelId === config.voiceChannelId + ) { + this.evaluateAloneStatus(); + } + }; + this.client.on(Events.VoiceStateUpdate, this.voiceStateListener); + + this.audioPlayer.on(AudioPlayerStatus.Idle, () => { + if (this.currentTrack) { + handleTrackCompletion(this.currentTrack); + } + this.currentTrack = null; + this.playNext(); + }); + + this.audioPlayer.on("error", (error) => { + console.error("Audio player encountered an error", error); + this.currentTrack = null; + this.playNext(); + }); + + this.audioPlayer.on("stateChange", (oldState, newState) => { + this.playbackStatus = newState.status; + this.emit("queueUpdate", this.snapshot()); + }); + } + + snapshot() { + return { + current: this.currentTrack, + upcoming: this.queue, + status: this.playbackStatus, + }; + } + + async ensureConnection() { + if ( + this.connection && + this.connection.joinConfig.channelId === config.voiceChannelId + ) { + return this.connection; + } + + let channel = this.client.channels.cache.get(config.voiceChannelId); + + if (!channel) { + try { + channel = await this.client.channels.fetch(config.voiceChannelId); + } catch (error) { + console.error("Unable to fetch voice channel", error); + throw error; + } + } + + if (!channel) { + throw new Error("Voice channel configured for music playback was not found"); + } + + const connection = joinVoiceChannel({ + channelId: channel.id, + guildId: channel.guild.id, + adapterCreator: channel.guild.voiceAdapterCreator, + selfDeaf: false, + }); + + this.connection = connection; + connection.subscribe(this.audioPlayer); + await entersState(connection, VoiceConnectionStatus.Ready, 30_000); + this.evaluateAloneStatus(); + return connection; + } + + enqueue(track) { + this.queue.push(track); + this.cancelIdleDisconnect(); + this.emit("queueUpdate", this.snapshot()); + if (!this.currentTrack) { + return this.playNext(); + } + + return track; + } + + async playNext() { + if (this.suspendedForAlone) { + return; + } + + if (this.queue.length === 0) { + this.scheduleIdleDisconnect(); + this.emit("queueUpdate", this.snapshot()); + return; + } + this.cancelIdleDisconnect(); + + const nextTrack = this.queue.shift(); + this.emit("queueUpdate", this.snapshot()); + + try { + await this.ensureConnection(); + } catch (error) { + console.error("Unable to connect to the configured voice channel", error); + deleteTrackFile(nextTrack); + this.currentTrack = null; + return; + } + + try { + const stream = fs.createReadStream(nextTrack.path); + const resource = createAudioResource(stream, { + inlineVolume: true, + }); + + this.currentTrack = nextTrack; + this.audioPlayer.play(resource); + this.emit("trackStart", nextTrack); + } catch (error) { + console.error("Unable to play track", error); + deleteTrackFile(nextTrack); + this.currentTrack = null; + return this.playNext(); + } + } + + async remove(trackId) { + const index = this.queue.findIndex((track) => track.id === trackId); + if (index === -1) { + if (this.currentTrack && this.currentTrack.id === trackId) { + this.skip(); + return this.currentTrack; + } + return null; + } + + const [removed] = this.queue.splice(index, 1); + deleteTrackFile(removed); + this.emit("queueUpdate", this.snapshot()); + if (this.queue.length === 0 && !this.currentTrack) { + this.scheduleIdleDisconnect(); + } + return removed; + } + + move(trackId, newPosition) { + const index = this.queue.findIndex((track) => track.id === trackId); + if (index === -1) { + return null; + } + + const [track] = this.queue.splice(index, 1); + const boundedPosition = Math.max(0, Math.min(newPosition, this.queue.length)); + this.queue.splice(boundedPosition, 0, track); + this.emit("queueUpdate", this.snapshot()); + return this.snapshot(); + } + + skip() { + this.audioPlayer.stop(true); + } + + stop() { + this.queue.forEach((track) => deleteTrackFile(track)); + this.queue = []; + this.audioPlayer.stop(true); + this.emit("queueUpdate", this.snapshot()); + } + + pause() { + this.audioPlayer.pause(true); + } + + resume() { + this.audioPlayer.unpause(); + } + + start() { + this.suspendedForAlone = false; + this.cancelIdleDisconnect(); + if (!this.currentTrack) { + return this.playNext(); + } + return this.currentTrack; + } + + removeByLibraryId(libraryTrackId) { + if (!libraryTrackId) { + return []; + } + + const removed = this.queue.filter( + (track) => track.libraryTrackId === libraryTrackId + ); + + this.queue = this.queue.filter( + (track) => track.libraryTrackId !== libraryTrackId + ); + + removed.forEach((track) => deleteTrackFile(track)); + + if ( + this.currentTrack && + this.currentTrack.libraryTrackId === libraryTrackId + ) { + this.skip(); + } + + if (removed.length > 0) { + this.emit("queueUpdate", this.snapshot()); + } + + return removed; + } + + destroyConnection(reason, { suspend = false } = {}) { + if (!this.connection && this.playbackStatus === AudioPlayerStatus.Idle) { + return; + } + + if (suspend) { + this.suspendedForAlone = true; + } + + this.cancelIdleDisconnect(); + this.cancelAloneDisconnect(); + + if (this.connection) { + try { + this.connection.destroy(); + } catch (error) { + console.error("Failed to destroy voice connection", error); + } + this.connection = null; + } + + try { + this.audioPlayer.stop(true); + } catch (error) { + console.error("Failed to stop audio player", error); + } + + this.currentTrack = null; + this.emit("queueUpdate", this.snapshot()); + console.log( + `[${new Date().toLocaleString()}] MusicQueue disconnected (${reason})` + ); + } + + scheduleIdleDisconnect() { + if ( + this.idleDisconnectTimeout || + this.queue.length > 0 || + this.currentTrack || + !this.connection + ) { + return; + } + + this.idleDisconnectTimeout = setTimeout(() => { + this.idleDisconnectTimeout = null; + if (!this.currentTrack && this.queue.length === 0 && this.connection) { + this.destroyConnection("idle timeout"); + } + }, AUTO_DISCONNECT_DELAY); + } + + cancelIdleDisconnect() { + if (this.idleDisconnectTimeout) { + clearTimeout(this.idleDisconnectTimeout); + this.idleDisconnectTimeout = null; + } + } + + scheduleAloneDisconnect() { + if (this.aloneDisconnectTimeout || !this.connection) { + return; + } + + this.aloneDisconnectTimeout = setTimeout(() => { + this.aloneDisconnectTimeout = null; + if (this.connection && this.isBotAlone()) { + this.destroyConnection("alone timeout", { suspend: true }); + } + }, AUTO_DISCONNECT_DELAY); + } + + cancelAloneDisconnect() { + if (this.aloneDisconnectTimeout) { + clearTimeout(this.aloneDisconnectTimeout); + this.aloneDisconnectTimeout = null; + } + } + + evaluateAloneStatus() { + if (!this.connection) { + this.cancelAloneDisconnect(); + return; + } + + if (this.isBotAlone()) { + this.scheduleAloneDisconnect(); + } else { + const wasSuspended = this.suspendedForAlone; + this.suspendedForAlone = false; + this.cancelAloneDisconnect(); + if (wasSuspended && !this.currentTrack && this.queue.length > 0) { + this.playNext(); + } + } + } + + isBotAlone() { + const channel = this.client.channels.cache.get(config.voiceChannelId); + if (!channel) { + return false; + } + + const clientId = this.client.user?.id; + if (!clientId) { + return false; + } + + if (!channel.members.has(clientId)) { + return false; + } + + const others = channel.members.filter((member) => member.id !== clientId); + return others.size === 0; + } +} + +export { MusicQueue }; diff --git a/src/web/middlewares/auth.js b/src/web/middlewares/auth.js new file mode 100644 index 0000000..11ca531 --- /dev/null +++ b/src/web/middlewares/auth.js @@ -0,0 +1,18 @@ +import { config } from "../../config.js"; + +function authMiddleware(req, res, next) { + if (!config.webApiKey) { + return next(); + } + + const header = req.headers.authorization || ""; + const token = header.replace("Bearer ", ""); + + if (token === config.webApiKey) { + return next(); + } + + return res.status(401).json({ error: "Unauthorized" }); +} + +export { authMiddleware }; diff --git a/src/web/routes/library.js b/src/web/routes/library.js new file mode 100644 index 0000000..07c79f1 --- /dev/null +++ b/src/web/routes/library.js @@ -0,0 +1,44 @@ +import { Router } from "express"; +import { + createQueueTrackFromLibrary, + deletePermanentTrack, + findPermanentTrack, + getPermanentLibrary, +} from "../storage/storageManager.js"; + +function libraryRouter({ queue }) { + const router = Router(); + + router.get("/", (req, res) => { + res.json({ tracks: getPermanentLibrary() }); + }); + + router.delete("/:libraryId", (req, res) => { + const track = deletePermanentTrack(req.params.libraryId); + + if (!track) { + return res.status(404).json({ error: "Fichier permanent introuvable" }); + } + + queue.removeByLibraryId(track.id); + res.json({ removed: track }); + }); + + router.post("/:libraryId/enqueue", (req, res) => { + const baseTrack = findPermanentTrack(req.params.libraryId); + if (!baseTrack) { + return res.status(404).json({ error: "Fichier permanent introuvable" }); + } + + const queueTrack = createQueueTrackFromLibrary( + baseTrack, + req.body?.requestedBy + ); + queue.enqueue(queueTrack); + res.status(201).json({ track: queueTrack }); + }); + + return router; +} + +export { libraryRouter }; diff --git a/src/web/routes/player.js b/src/web/routes/player.js new file mode 100644 index 0000000..011d383 --- /dev/null +++ b/src/web/routes/player.js @@ -0,0 +1,34 @@ +import { Router } from "express"; + +function playerRouter({ queue }) { + const router = Router(); + + router.post("/play", async (req, res) => { + await queue.start(); + res.json(queue.snapshot()); + }); + + router.post("/pause", (req, res) => { + queue.pause(); + res.json(queue.snapshot()); + }); + + router.post("/resume", (req, res) => { + queue.resume(); + res.json(queue.snapshot()); + }); + + router.post("/skip", (req, res) => { + queue.skip(); + res.json(queue.snapshot()); + }); + + router.post("/stop", (req, res) => { + queue.stop(); + res.json(queue.snapshot()); + }); + + return router; +} + +export { playerRouter }; diff --git a/src/web/routes/queue.js b/src/web/routes/queue.js new file mode 100644 index 0000000..a35a58a --- /dev/null +++ b/src/web/routes/queue.js @@ -0,0 +1,56 @@ +import { Router } from "express"; +import { createQueueTrackFromLibrary, findPermanentTrack } from "../storage/storageManager.js"; + +function queueRouter({ queue }) { + const router = Router(); + + router.get("/", (req, res) => { + res.json(queue.snapshot()); + }); + + router.post("/", (req, res) => { + const { libraryTrackId, requestedBy } = req.body; + + if (!libraryTrackId) { + return res.status(400).json({ error: "libraryTrackId manquant" }); + } + + const libraryTrack = findPermanentTrack(libraryTrackId); + + if (!libraryTrack) { + return res.status(404).json({ error: "Fichier permanent introuvable" }); + } + + const track = createQueueTrackFromLibrary(libraryTrack, requestedBy); + queue.enqueue(track); + res.status(201).json({ track }); + }); + + router.delete("/:trackId", async (req, res) => { + const removed = await queue.remove(req.params.trackId); + if (!removed) { + return res.status(404).json({ error: "Piste introuvable" }); + } + + res.json({ removed }); + }); + + router.patch("/:trackId/move", (req, res) => { + const position = Number(req.body?.position); + if (Number.isNaN(position)) { + return res.status(400).json({ error: "position doit être un nombre" }); + } + + const snapshot = queue.move(req.params.trackId, position); + + if (!snapshot) { + return res.status(404).json({ error: "Piste introuvable" }); + } + + res.json(snapshot); + }); + + return router; +} + +export { queueRouter }; diff --git a/src/web/routes/uploads.js b/src/web/routes/uploads.js new file mode 100644 index 0000000..2daf273 --- /dev/null +++ b/src/web/routes/uploads.js @@ -0,0 +1,40 @@ +import { Router } from "express"; +import multer from "multer"; +import { TEMP_DIR, registerUploadedFile } from "../storage/storageManager.js"; + +const upload = multer({ + dest: TEMP_DIR, + limits: { + fileSize: 150 * 1024 * 1024, + }, +}); + +function uploadsRouter({ queue }) { + const router = Router(); + + router.post("/", upload.single("file"), async (req, res, next) => { + if (!req.file) { + return res.status(400).json({ error: "Aucun fichier reçu" }); + } + + const permanent = req.body?.permanent === "true"; + const enqueue = req.body?.enqueue !== "false"; + const uploader = req.body?.uploader || "web-ui"; + + try { + const track = registerUploadedFile(req.file, { permanent, uploader }); + + if (enqueue) { + queue.enqueue(track); + } + + return res.status(201).json({ track, enqueued: enqueue }); + } catch (error) { + return next(error); + } + }); + + return router; +} + +export { uploadsRouter }; diff --git a/src/web/server.js b/src/web/server.js new file mode 100644 index 0000000..b0868c2 --- /dev/null +++ b/src/web/server.js @@ -0,0 +1,43 @@ +import express from "express"; +import path from "path"; +import { config } from "../config.js"; +import { authMiddleware } from "./middlewares/auth.js"; +import { libraryRouter } from "./routes/library.js"; +import { playerRouter } from "./routes/player.js"; +import { queueRouter } from "./routes/queue.js"; +import { uploadsRouter } from "./routes/uploads.js"; +import { ensureStorageLayout } from "./storage/storageManager.js"; + +const PUBLIC_DIR = path.join(process.cwd(), "public"); + +function startWebServer(dependencies) { + ensureStorageLayout(); + + const app = express(); + app.use(express.json()); + + const apiRouter = express.Router(); + apiRouter.use(authMiddleware); + apiRouter.use("/uploads", uploadsRouter(dependencies)); + apiRouter.use("/queue", queueRouter(dependencies)); + apiRouter.use("/library", libraryRouter(dependencies)); + apiRouter.use("/player", playerRouter(dependencies)); + + app.use("/api", apiRouter); + app.get(["/jukebox", "/jukebox/"], (_req, res) => + res.sendFile(path.join(PUBLIC_DIR, "jukebox.html")) + ); + app.use(express.static(PUBLIC_DIR)); + + const port = Number(process.env.PORT || config.webServerPort || 3000); + + app.listen(port, () => { + console.log( + `[${new Date().toLocaleString()}] Web control server listening on port ${port}` + ); + }); + + return app; +} + +export { startWebServer }; diff --git a/src/web/storage/storageManager.js b/src/web/storage/storageManager.js new file mode 100644 index 0000000..079d594 --- /dev/null +++ b/src/web/storage/storageManager.js @@ -0,0 +1,166 @@ +import fs from "fs"; +import path from "path"; +import { v4 as uuid } from "uuid"; + +const STORAGE_ROOT = path.join(process.cwd(), "storage"); +const PERMANENT_DIR = path.join(STORAGE_ROOT, "permanent"); +const TEMP_DIR = path.join(STORAGE_ROOT, "temp"); +const LIBRARY_FILE = path.join(STORAGE_ROOT, "library.json"); + +function ensureStorageLayout() { + if (!fs.existsSync(STORAGE_ROOT)) { + fs.mkdirSync(STORAGE_ROOT, { recursive: true }); + } + + if (!fs.existsSync(PERMANENT_DIR)) { + fs.mkdirSync(PERMANENT_DIR, { recursive: true }); + } + + if (!fs.existsSync(TEMP_DIR)) { + fs.mkdirSync(TEMP_DIR, { recursive: true }); + } + + if (!fs.existsSync(LIBRARY_FILE)) { + fs.writeFileSync(LIBRARY_FILE, JSON.stringify([])); + } +} + +function readLibrary() { + try { + const raw = fs.readFileSync(LIBRARY_FILE, { encoding: "utf8" }); + return JSON.parse(raw); + } catch (error) { + console.error("Failed to read library file", error); + return []; + } +} + +function writeLibrary(entries) { + fs.writeFileSync(LIBRARY_FILE, JSON.stringify(entries, null, 2)); +} + +function registerUploadedFile(file, { permanent, uploader = "unknown" }) { + ensureStorageLayout(); + const createdAt = new Date().toISOString(); + let finalPath = file.path; + let storedName = path.basename(file.path); + let permanentEntry = null; + + if (permanent) { + const extension = path.extname(file.originalname) || ""; + storedName = `${uuid()}${extension}`; + finalPath = path.join(PERMANENT_DIR, storedName); + fs.renameSync(file.path, finalPath); + permanentEntry = { + id: uuid(), + originalName: file.originalname, + storedName, + path: finalPath, + size: file.size, + mimetype: file.mimetype, + uploader, + createdAt, + }; + const library = readLibrary(); + library.push(permanentEntry); + writeLibrary(library); + } + + return { + id: uuid(), + title: file.originalname, + originalName: file.originalname, + path: finalPath, + storedName, + size: file.size, + mimetype: file.mimetype, + permanent, + uploader, + libraryTrackId: permanentEntry ? permanentEntry.id : null, + createdAt, + }; +} + +function getPermanentLibrary() { + ensureStorageLayout(); + return readLibrary(); +} + +function findPermanentTrack(id) { + return getPermanentLibrary().find((entry) => entry.id === id); +} + +function createQueueTrackFromLibrary(libraryTrack, requestedBy = "library") { + if (!libraryTrack) { + return null; + } + + return { + id: uuid(), + title: libraryTrack.originalName, + originalName: libraryTrack.originalName, + path: libraryTrack.path, + storedName: libraryTrack.storedName, + size: libraryTrack.size, + mimetype: libraryTrack.mimetype, + permanent: true, + uploader: requestedBy, + libraryTrackId: libraryTrack.id, + createdAt: new Date().toISOString(), + }; +} + +function deleteFileSafe(filePath) { + if (!filePath) { + return; + } + + try { + fs.existsSync(filePath) && fs.unlinkSync(filePath); + } catch (error) { + console.error(`Failed to delete file ${filePath}`, error); + } +} + +function deletePermanentTrack(id) { + const library = getPermanentLibrary(); + const index = library.findIndex((entry) => entry.id === id); + if (index === -1) { + return null; + } + + const [removed] = library.splice(index, 1); + writeLibrary(library); + deleteFileSafe(removed.path); + return removed; +} + +function handleTrackCompletion(track) { + if (!track || track.permanent) { + return; + } + deleteFileSafe(track.path); +} + +function deleteTrackFile(track) { + if (!track) { + return; + } + + if (!track.permanent) { + deleteFileSafe(track.path); + } +} + +export { + PERMANENT_DIR, + TEMP_DIR, + ensureStorageLayout, + registerUploadedFile, + getPermanentLibrary, + findPermanentTrack, + createQueueTrackFromLibrary, + deletePermanentTrack, + handleTrackCompletion, + deleteTrackFile, +}; diff --git a/storage/library.json b/storage/library.json new file mode 100644 index 0000000..cf447c4 --- /dev/null +++ b/storage/library.json @@ -0,0 +1,32 @@ +[ + { + "id": "67e8cb3f-c079-4961-b9ff-4b3b16cd8dc5", + "originalName": "Monster Hunter Wilds [OST] - Credits Music (Wyverian Song).mp3", + "storedName": "7d39cc0e-3a29-43c4-b265-1eb15b9b52b9.mp3", + "path": "/home/shinuwa/projects/palico-bot/storage/permanent/7d39cc0e-3a29-43c4-b265-1eb15b9b52b9.mp3", + "size": 3813641, + "mimetype": "audio/mpeg", + "uploader": "Shinuwa", + "createdAt": "2025-11-18T16:48:46.726Z" + }, + { + "id": "2b461bf4-2917-4faa-ae39-1111b6c478f7", + "originalName": "Dark Hallway.mp3", + "storedName": "3f4c096b-e316-4614-86f7-3f223ad9e362.mp3", + "path": "/home/shinuwa/projects/palico-bot/storage/permanent/3f4c096b-e316-4614-86f7-3f223ad9e362.mp3", + "size": 3090274, + "mimetype": "audio/mpeg", + "uploader": "Shinuwa", + "createdAt": "2025-11-18T16:50:42.461Z" + }, + { + "id": "c96b0e12-df38-4967-aeca-96335a3cf2a2", + "originalName": "Nintendo - Christmas Mix.mp3", + "storedName": "3c790e19-35a2-4e04-9956-736cb34e010a.mp3", + "path": "/home/shinuwa/projects/palico-bot/storage/permanent/3c790e19-35a2-4e04-9956-736cb34e010a.mp3", + "size": 122621211, + "mimetype": "audio/mpeg", + "uploader": "Shinuwa", + "createdAt": "2025-11-18T20:50:10.029Z" + } +] \ No newline at end of file diff --git a/storage/permanent/.gitkeep b/storage/permanent/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/storage/temp/.gitkeep b/storage/temp/.gitkeep new file mode 100644 index 0000000..e69de29