mirror of
https://github.com/arthur-pbty/flint.git
synced 2026-08-01 20:29:03 +02:00
feat: implement utility commands and command framework
- Add advanced command for complex argument handling. - Introduce ping command for latency checking. - Create argument parser for prefix and slash commands. - Define command structure and registry for command management. - Implement command deployment for Discord application commands. - Add internationalization support for commands. - Create handlers for prefix and slash command execution. - Establish command execution context and permissions handling. - Set up environment configuration with dotenv and zod. - Add TypeScript configuration for strict type checking.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
DISCORD_TOKEN=
|
||||
DISCORD_CLIENT_ID=
|
||||
PREFIX=+
|
||||
DEFAULT_LANG=en
|
||||
DEV_GUILD_ID=
|
||||
AUTO_DEPLOY_SLASH=false
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.vscode/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
@@ -0,0 +1,53 @@
|
||||
# Discord.js v14 Framework Template
|
||||
|
||||
Professional command framework template for Discord.js `14.26.2` with:
|
||||
|
||||
- Single command object schema
|
||||
- Minimal command authoring (`name`, `category`, `execute`)
|
||||
- Shared execution logic for prefix and slash
|
||||
- External JSON i18n
|
||||
- Automatic prefix and slash localizations from locale files
|
||||
- One localized `name` per language drives both prefix and slash triggers
|
||||
- Typed argument schema
|
||||
- User permission checks
|
||||
- Auto-generated help from command metadata
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
npm install
|
||||
2. Create environment file:
|
||||
cp .env.example .env
|
||||
3. Fill required values in `.env`:
|
||||
- `DISCORD_TOKEN`
|
||||
- `DISCORD_CLIENT_ID`
|
||||
4. Deploy slash commands:
|
||||
npm run deploy:commands
|
||||
5. Start in development:
|
||||
npm run dev
|
||||
|
||||
## Architecture
|
||||
|
||||
- `src/framework/types/command.ts`: strict command schema
|
||||
- `src/framework/commands/defineCommand.ts`: default command completion
|
||||
- `src/framework/commands/registry.ts`: trigger/name mapping generated from locales
|
||||
- `src/framework/commands/argParser.ts`: prefix/slash args parsing from schema
|
||||
- `src/framework/execution/CommandExecutor.ts`: unified pipeline (permissions/execute)
|
||||
- `src/framework/handlers/prefixHandler.ts`: prefix entrypoint
|
||||
- `src/framework/handlers/slashHandler.ts`: slash entrypoint
|
||||
- `locales/*.json`: external i18n dictionaries
|
||||
- `src/commands/*`: business commands only (`execute`)
|
||||
|
||||
## Included Commands
|
||||
|
||||
- `kiss` (`fun`) with required `user` arg
|
||||
- `ping` (`utility`)
|
||||
- `advanced` (`utility`) with full argument/permission example
|
||||
- `help` (`core`) with auto category and usage generation
|
||||
|
||||
## Adding A Command
|
||||
|
||||
1. Create a command object in `src/commands/...`
|
||||
2. Follow the schema in `src/framework/types/command.ts`
|
||||
3. Add command to `src/commands/index.ts`
|
||||
4. Deploy slash commands again (`npm run deploy:commands`)
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"errors": {
|
||||
"args": {
|
||||
"missing": "Missing argument {{arg}}. Usage: {{usage}}",
|
||||
"invalidInt": "Argument must be an integer. Received: {{value}}",
|
||||
"invalidNumber": "Argument must be a number. Received: {{value}}",
|
||||
"invalidBoolean": "Argument must be a boolean value. Received: {{value}}",
|
||||
"invalidUser": "Could not resolve a user from {{value}}"
|
||||
},
|
||||
"permissions": {
|
||||
"user": "You are missing permissions: {{permissions}}"
|
||||
},
|
||||
"execution": "An unexpected error happened while running this command."
|
||||
},
|
||||
"categories": {
|
||||
"fun": "Fun",
|
||||
"utility": "Utility",
|
||||
"core": "Core"
|
||||
},
|
||||
"commands": {
|
||||
"kiss": {
|
||||
"name": "kiss",
|
||||
"description": "Send a kiss to another user.",
|
||||
"args": {
|
||||
"user": "Target user"
|
||||
},
|
||||
"examples": {
|
||||
"basic": "Send a kiss to a member"
|
||||
},
|
||||
"responses": {
|
||||
"success": "{{from}} sends a kiss to {{to}}."
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"name": "ping",
|
||||
"description": "Check bot websocket latency.",
|
||||
"examples": {
|
||||
"basic": "Basic health check"
|
||||
},
|
||||
"responses": {
|
||||
"pong": "Pong. Websocket latency: {{latency}}ms."
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"name": "advanced",
|
||||
"description": "Showcase command using every available command parameter.",
|
||||
"args": {
|
||||
"text": "Required text value",
|
||||
"count": "Required integer value",
|
||||
"ratio": "Optional decimal number",
|
||||
"enabled": "Optional boolean flag",
|
||||
"user": "Required target user",
|
||||
"channel": "Optional channel",
|
||||
"role": "Optional role"
|
||||
},
|
||||
"examples": {
|
||||
"full": "Complete prefix example with all argument types",
|
||||
"slash": "Equivalent slash usage"
|
||||
},
|
||||
"responses": {
|
||||
"summary": "Advanced command summary\ntext={{text}}\ncount={{count}}\nratio={{ratio}}\nenabled={{enabled}}\nuser={{user}}\nchannel={{channel}}\nrole={{role}}\nsource={{source}}"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"name": "help",
|
||||
"description": "Display command list or details for one command.",
|
||||
"args": {
|
||||
"command": "Optional command name or trigger"
|
||||
},
|
||||
"examples": {
|
||||
"basic": "List all commands",
|
||||
"single": "Show details for one command"
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "No command matches {{query}}."
|
||||
},
|
||||
"embed": {
|
||||
"title": "Command Help",
|
||||
"description": "Use {{usage}} to inspect one command.",
|
||||
"categoryEmpty": "No commands in this category.",
|
||||
"detailsTitle": "Command: {{name}}",
|
||||
"detailsDescription": "{{description}}",
|
||||
"footer": "Category: {{source}}",
|
||||
"fields": {
|
||||
"usage": "Usage",
|
||||
"arguments": "Arguments",
|
||||
"examples": "Examples"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"prefix": "Prefix",
|
||||
"slash": "Slash",
|
||||
"required": "required",
|
||||
"optional": "optional",
|
||||
"noArgs": "No arguments.",
|
||||
"noExamples": "No examples."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"errors": {
|
||||
"args": {
|
||||
"missing": "Falta el argumento {{arg}}. Uso: {{usage}}",
|
||||
"invalidInt": "El argumento debe ser un entero. Recibido: {{value}}",
|
||||
"invalidNumber": "El argumento debe ser un numero. Recibido: {{value}}",
|
||||
"invalidBoolean": "El argumento debe ser un valor booleano. Recibido: {{value}}",
|
||||
"invalidUser": "No se pudo resolver un usuario desde {{value}}"
|
||||
},
|
||||
"permissions": {
|
||||
"user": "Te faltan permisos: {{permissions}}"
|
||||
},
|
||||
"execution": "Ocurrio un error inesperado al ejecutar este comando."
|
||||
},
|
||||
"categories": {
|
||||
"fun": "Diversion",
|
||||
"utility": "Utilidad",
|
||||
"core": "Nucleo"
|
||||
},
|
||||
"commands": {
|
||||
"kiss": {
|
||||
"name": "beso",
|
||||
"description": "Enviar un beso a otro usuario.",
|
||||
"args": {
|
||||
"user": "Usuario objetivo"
|
||||
},
|
||||
"examples": {
|
||||
"basic": "Enviar un beso a un miembro"
|
||||
},
|
||||
"responses": {
|
||||
"success": "{{from}} envia un beso a {{to}}."
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"name": "ping",
|
||||
"description": "Revisar latencia websocket del bot.",
|
||||
"examples": {
|
||||
"basic": "Chequeo basico"
|
||||
},
|
||||
"responses": {
|
||||
"pong": "Pong. Latencia websocket: {{latency}}ms."
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"name": "avanzada",
|
||||
"description": "Comando de demostracion que usa todos los parametros disponibles.",
|
||||
"args": {
|
||||
"text": "Texto obligatorio",
|
||||
"count": "Entero obligatorio",
|
||||
"ratio": "Numero decimal opcional",
|
||||
"enabled": "Bandera booleana opcional",
|
||||
"user": "Usuario objetivo obligatorio",
|
||||
"channel": "Canal opcional",
|
||||
"role": "Rol opcional"
|
||||
},
|
||||
"examples": {
|
||||
"full": "Ejemplo prefix completo con todos los tipos de argumentos",
|
||||
"slash": "Equivalente en slash"
|
||||
},
|
||||
"responses": {
|
||||
"summary": "Resumen comando avanzada\ntext={{text}}\ncount={{count}}\nratio={{ratio}}\nenabled={{enabled}}\nuser={{user}}\nchannel={{channel}}\nrole={{role}}\nsource={{source}}"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"name": "ayuda",
|
||||
"description": "Mostrar lista de comandos o detalles de un comando.",
|
||||
"args": {
|
||||
"command": "Nombre o trigger opcional del comando"
|
||||
},
|
||||
"examples": {
|
||||
"basic": "Listar todos los comandos",
|
||||
"single": "Mostrar detalles de un comando"
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Ningun comando coincide con {{query}}."
|
||||
},
|
||||
"embed": {
|
||||
"title": "Ayuda de Comandos",
|
||||
"description": "Usa {{usage}} para inspeccionar un comando.",
|
||||
"categoryEmpty": "No hay comandos en esta categoria.",
|
||||
"detailsTitle": "Comando: {{name}}",
|
||||
"detailsDescription": "{{description}}",
|
||||
"footer": "Categoria: {{source}}",
|
||||
"fields": {
|
||||
"usage": "Uso",
|
||||
"arguments": "Argumentos",
|
||||
"examples": "Ejemplos"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"prefix": "Prefijo",
|
||||
"slash": "Slash",
|
||||
"required": "requerido",
|
||||
"optional": "opcional",
|
||||
"noArgs": "Sin argumentos.",
|
||||
"noExamples": "Sin ejemplos."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
{
|
||||
"errors": {
|
||||
"args": {
|
||||
"missing": "Argument manquant {{arg}}. Usage: {{usage}}",
|
||||
"invalidInt": "L argument doit etre un entier. Recu: {{value}}",
|
||||
"invalidNumber": "L argument doit etre un nombre. Recu: {{value}}",
|
||||
"invalidBoolean": "L argument doit etre un booleen. Recu: {{value}}",
|
||||
"invalidUser": "Impossible de trouver un utilisateur depuis {{value}}"
|
||||
},
|
||||
"permissions": {
|
||||
"user": "Tu n as pas les permissions: {{permissions}}"
|
||||
},
|
||||
"execution": "Une erreur inattendue est survenue pendant l execution."
|
||||
},
|
||||
"categories": {
|
||||
"fun": "Fun",
|
||||
"utility": "Utilitaire",
|
||||
"core": "Base"
|
||||
},
|
||||
"commands": {
|
||||
"kiss": {
|
||||
"name": "bisou",
|
||||
"description": "Envoyer un bisou a un utilisateur.",
|
||||
"args": {
|
||||
"user": "Utilisateur cible"
|
||||
},
|
||||
"examples": {
|
||||
"basic": "Envoyer un bisou a un membre"
|
||||
},
|
||||
"responses": {
|
||||
"success": "{{from}} envoie un bisou a {{to}}."
|
||||
}
|
||||
},
|
||||
"ping": {
|
||||
"name": "ping",
|
||||
"description": "Verifier la latence websocket du bot.",
|
||||
"examples": {
|
||||
"basic": "Verification de sante"
|
||||
},
|
||||
"responses": {
|
||||
"pong": "Pong. Latence websocket: {{latency}}ms."
|
||||
}
|
||||
},
|
||||
"advanced": {
|
||||
"name": "avance",
|
||||
"description": "Commande de demonstration utilisant tous les parametres disponibles.",
|
||||
"args": {
|
||||
"text": "Texte requis",
|
||||
"count": "Entier requis",
|
||||
"ratio": "Nombre decimal optionnel",
|
||||
"enabled": "Booleen optionnel",
|
||||
"user": "Utilisateur cible requis",
|
||||
"channel": "Salon optionnel",
|
||||
"role": "Role optionnel"
|
||||
},
|
||||
"examples": {
|
||||
"full": "Exemple prefix complet avec tous les types d arguments",
|
||||
"slash": "Equivalent en slash"
|
||||
},
|
||||
"responses": {
|
||||
"summary": "Resume commande avancee\ntext={{text}}\ncount={{count}}\nratio={{ratio}}\nenabled={{enabled}}\nuser={{user}}\nchannel={{channel}}\nrole={{role}}\nsource={{source}}"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"name": "aide",
|
||||
"description": "Afficher la liste des commandes ou les details d une commande.",
|
||||
"args": {
|
||||
"command": "Nom ou trigger de commande optionnel"
|
||||
},
|
||||
"examples": {
|
||||
"basic": "Afficher toutes les commandes",
|
||||
"single": "Afficher les details d une commande"
|
||||
},
|
||||
"errors": {
|
||||
"notFound": "Aucune commande ne correspond a {{query}}."
|
||||
},
|
||||
"embed": {
|
||||
"title": "Aide Commandes",
|
||||
"description": "Utilise {{usage}} pour inspecter une commande.",
|
||||
"categoryEmpty": "Aucune commande dans cette categorie.",
|
||||
"detailsTitle": "Commande: {{name}}",
|
||||
"detailsDescription": "{{description}}",
|
||||
"footer": "Categorie: {{source}}",
|
||||
"fields": {
|
||||
"usage": "Usage",
|
||||
"arguments": "Arguments",
|
||||
"examples": "Exemples"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"prefix": "Prefix",
|
||||
"slash": "Slash",
|
||||
"required": "requis",
|
||||
"optional": "optionnel",
|
||||
"noArgs": "Aucun argument.",
|
||||
"noExamples": "Aucun exemple."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+875
@@ -0,0 +1,875 @@
|
||||
{
|
||||
"name": "discordjs-framework-template",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "discordjs-framework-template",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"discord.js": "14.26.2",
|
||||
"dotenv": "16.4.5",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.17.24",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
|
||||
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"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/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.2",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz",
|
||||
"integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.5",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.24.1"
|
||||
},
|
||||
"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/@sapphire/snowflake": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
||||
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"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/@esbuild/aix-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz",
|
||||
"integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz",
|
||||
"integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz",
|
||||
"integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz",
|
||||
"integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz",
|
||||
"integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz",
|
||||
"integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz",
|
||||
"integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz",
|
||||
"integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz",
|
||||
"integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"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": "20.17.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
|
||||
"integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
},
|
||||
"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/discord-api-types": {
|
||||
"version": "0.38.45",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.45.tgz",
|
||||
"integrity": "sha512-DiI01i00FPv6n+hXcFkFxK8Y/rFRpKs6U6aP32N4T73nTbj37Eua3H/95TBpLktLWB6xnLXhYDGvyLq6zzYY2w==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"scripts/actions/documentation"
|
||||
]
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.26.2",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.2.tgz",
|
||||
"integrity": "sha512-feShi+gULJ6R2MAA4/KkCFnkJcuVrROJrKk4czplzq8gE1oqhqgOy9K0Scu44B8oGeWKe04egquzf+ia6VtXAw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.14.1",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/rest": "^2.6.1",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.3",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "6.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.23.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz",
|
||||
"integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.23.1",
|
||||
"@esbuild/android-arm": "0.23.1",
|
||||
"@esbuild/android-arm64": "0.23.1",
|
||||
"@esbuild/android-x64": "0.23.1",
|
||||
"@esbuild/darwin-arm64": "0.23.1",
|
||||
"@esbuild/darwin-x64": "0.23.1",
|
||||
"@esbuild/freebsd-arm64": "0.23.1",
|
||||
"@esbuild/freebsd-x64": "0.23.1",
|
||||
"@esbuild/linux-arm": "0.23.1",
|
||||
"@esbuild/linux-arm64": "0.23.1",
|
||||
"@esbuild/linux-ia32": "0.23.1",
|
||||
"@esbuild/linux-loong64": "0.23.1",
|
||||
"@esbuild/linux-mips64el": "0.23.1",
|
||||
"@esbuild/linux-ppc64": "0.23.1",
|
||||
"@esbuild/linux-riscv64": "0.23.1",
|
||||
"@esbuild/linux-s390x": "0.23.1",
|
||||
"@esbuild/linux-x64": "0.23.1",
|
||||
"@esbuild/netbsd-x64": "0.23.1",
|
||||
"@esbuild/openbsd-arm64": "0.23.1",
|
||||
"@esbuild/openbsd-x64": "0.23.1",
|
||||
"@esbuild/sunos-x64": "0.23.1",
|
||||
"@esbuild/win32-arm64": "0.23.1",
|
||||
"@esbuild/win32-ia32": "0.23.1",
|
||||
"@esbuild/win32-x64": "0.23.1"
|
||||
}
|
||||
},
|
||||
"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/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.13.7",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
|
||||
"integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"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.13.0",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz",
|
||||
"integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"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/tsx": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.2.tgz",
|
||||
"integrity": "sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.23.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
|
||||
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
|
||||
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
|
||||
"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/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "discordjs-framework-template",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js",
|
||||
"deploy:commands": "tsx src/scripts/deployCommands.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord.js": "14.26.2",
|
||||
"dotenv": "16.4.5",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.17.24",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.8.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
|
||||
import { buildPrefixUsage, buildSlashUsage, resolvePrefixTrigger, resolveSlashName } from "../../framework/commands/usage.js";
|
||||
import { defineCommand } from "../../framework/commands/defineCommand.js";
|
||||
import type { BotCommand, CommandExecutionContext } from "../../framework/types/command.js";
|
||||
|
||||
const categoryName = (command: BotCommand): string => command.meta.category;
|
||||
|
||||
const commandDescription = (ctx: CommandExecutionContext, command: BotCommand): string =>
|
||||
ctx.i18n.commandT(ctx.lang, command.meta.name, "description");
|
||||
|
||||
const resolveCommandFromQuery = (ctx: CommandExecutionContext, query: string): BotCommand | undefined => {
|
||||
const normalized = query.toLowerCase();
|
||||
const byName = ctx.registry.findByName(normalized);
|
||||
if (byName) {
|
||||
return byName;
|
||||
}
|
||||
|
||||
return ctx.registry.findByAnyPrefixTrigger(normalized)?.command;
|
||||
};
|
||||
|
||||
const buildGlobalHelpEmbed = (ctx: CommandExecutionContext): EmbedBuilder => {
|
||||
const commands = [...ctx.registry.getAll()];
|
||||
const grouped = new Map<string, BotCommand[]>();
|
||||
|
||||
for (const command of commands) {
|
||||
const key = categoryName(command);
|
||||
if (!grouped.has(key)) {
|
||||
grouped.set(key, []);
|
||||
}
|
||||
grouped.get(key)?.push(command);
|
||||
}
|
||||
|
||||
const helpUsage = buildPrefixUsage(helpCommand, ctx.prefix, ctx.lang, ctx.defaultLang, ctx.i18n);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x2b6cb0)
|
||||
.setTitle(ctx.ct("embed.title"))
|
||||
.setDescription(ctx.ct("embed.description", { prefix: ctx.prefix, usage: `${helpUsage} <command>` }));
|
||||
|
||||
for (const [category, categoryCommands] of grouped.entries()) {
|
||||
const lines = categoryCommands
|
||||
.map((command) => {
|
||||
const slashLabel = `/${resolveSlashName(command, ctx.lang, ctx.i18n)}`;
|
||||
const prefixLabel = `${ctx.prefix}${resolvePrefixTrigger(command, ctx.lang, ctx.defaultLang, ctx.i18n)}`;
|
||||
return `${slashLabel} | ${prefixLabel} - ${commandDescription(ctx, command)}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
embed.addFields({
|
||||
name: ctx.t(`categories.${category}`),
|
||||
value: lines.length > 0 ? lines : ctx.ct("embed.categoryEmpty"),
|
||||
});
|
||||
}
|
||||
|
||||
return embed;
|
||||
};
|
||||
|
||||
const buildCommandDetailsEmbed = (ctx: CommandExecutionContext, command: BotCommand): EmbedBuilder => {
|
||||
const usagePrefix = buildPrefixUsage(command, ctx.prefix, ctx.lang, ctx.defaultLang, ctx.i18n);
|
||||
const usageSlash = buildSlashUsage(command, ctx.lang, ctx.i18n);
|
||||
const prefixTrigger = resolvePrefixTrigger(command, ctx.lang, ctx.defaultLang, ctx.i18n);
|
||||
const slashTrigger = resolveSlashName(command, ctx.lang, ctx.i18n);
|
||||
const localizedCommandName = ctx.i18n.commandName(ctx.lang, command.meta.name);
|
||||
|
||||
const args = command.args.length === 0
|
||||
? ctx.ct("labels.noArgs")
|
||||
: command.args
|
||||
.map((arg) => {
|
||||
const description = ctx.i18n.commandT(ctx.lang, command.meta.name, arg.descriptionKey);
|
||||
const requirement = arg.required ? ctx.ct("labels.required") : ctx.ct("labels.optional");
|
||||
return `- ${arg.name} (${arg.type}, ${requirement}): ${description}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const examples = command.examples.length === 0
|
||||
? ctx.ct("labels.noExamples")
|
||||
: command.examples
|
||||
.map((example) => {
|
||||
const source = example.source ?? "prefix";
|
||||
const baseUsage = source === "slash"
|
||||
? buildSlashUsage(command, ctx.lang, ctx.i18n)
|
||||
: buildPrefixUsage(command, ctx.prefix, ctx.lang, ctx.defaultLang, ctx.i18n);
|
||||
|
||||
const baseCommand = source === "slash"
|
||||
? `/${slashTrigger}`
|
||||
: `${ctx.prefix}${prefixTrigger}`;
|
||||
|
||||
const input = example.args ? `${baseCommand} ${example.args}` : baseUsage;
|
||||
const description = ctx.i18n.commandT(ctx.lang, command.meta.name, example.descriptionKey);
|
||||
return `- ${input}: ${description}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return new EmbedBuilder()
|
||||
.setColor(0x2f855a)
|
||||
.setTitle(ctx.ct("embed.detailsTitle", { name: localizedCommandName }))
|
||||
.setDescription(ctx.ct("embed.detailsDescription", { description: commandDescription(ctx, command) }))
|
||||
.addFields(
|
||||
{
|
||||
name: ctx.ct("embed.fields.usage"),
|
||||
value: `${ctx.ct("labels.prefix")}: ${usagePrefix}\n${ctx.ct("labels.slash")}: ${usageSlash}`,
|
||||
},
|
||||
{
|
||||
name: ctx.ct("embed.fields.arguments"),
|
||||
value: args,
|
||||
},
|
||||
{
|
||||
name: ctx.ct("embed.fields.examples"),
|
||||
value: examples,
|
||||
},
|
||||
)
|
||||
.setFooter({ text: ctx.ct("embed.footer", { source: command.meta.category }) });
|
||||
};
|
||||
|
||||
export const helpCommand = defineCommand({
|
||||
meta: {
|
||||
name: "help",
|
||||
category: "core",
|
||||
},
|
||||
args: [
|
||||
{
|
||||
name: "command",
|
||||
type: "string",
|
||||
required: false,
|
||||
descriptionKey: "args.command",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
{
|
||||
descriptionKey: "examples.basic",
|
||||
},
|
||||
{
|
||||
args: "<command>",
|
||||
descriptionKey: "examples.single",
|
||||
},
|
||||
],
|
||||
execute: async (ctx) => {
|
||||
const queryArg = ctx.args.command;
|
||||
|
||||
if (typeof queryArg === "string" && queryArg.trim().length > 0) {
|
||||
const command = resolveCommandFromQuery(ctx, queryArg.trim());
|
||||
if (!command) {
|
||||
await ctx.reply(ctx.ct("errors.notFound", { query: queryArg }));
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.reply({ embeds: [buildCommandDetailsEmbed(ctx, command)] });
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.reply({ embeds: [buildGlobalHelpEmbed(ctx)] });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineCommand } from "../../framework/commands/defineCommand.js";
|
||||
|
||||
export const kissCommand = defineCommand({
|
||||
meta: {
|
||||
name: "kiss",
|
||||
category: "fun",
|
||||
},
|
||||
args: [
|
||||
{
|
||||
name: "user",
|
||||
type: "user",
|
||||
required: true,
|
||||
descriptionKey: "args.user",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
{
|
||||
args: "@Arthur",
|
||||
descriptionKey: "examples.basic",
|
||||
},
|
||||
],
|
||||
execute: async (ctx) => {
|
||||
const target = ctx.args.user;
|
||||
if (!target || typeof target !== "object" || !("id" in target)) {
|
||||
await ctx.reply(ctx.t("errors.args.invalidUser", { value: String(target) }));
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.reply(
|
||||
ctx.ct("responses.success", {
|
||||
from: `<@${ctx.user.id}>`,
|
||||
to: `<@${target.id}>`,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { helpCommand } from "./core/help.js";
|
||||
import { kissCommand } from "./fun/kiss.js";
|
||||
import { advancedCommand } from "./utility/advanced.js";
|
||||
import { pingCommand } from "./utility/ping.js";
|
||||
|
||||
import type { BotCommand } from "../framework/types/command.js";
|
||||
|
||||
export const commandList: BotCommand[] = [kissCommand, pingCommand, advancedCommand, helpCommand];
|
||||
@@ -0,0 +1,106 @@
|
||||
import { PermissionFlagsBits } from "discord.js";
|
||||
|
||||
import { defineCommand } from "../../framework/commands/defineCommand.js";
|
||||
|
||||
const extractId = (value: unknown): string | null => {
|
||||
if (!value || typeof value !== "object" || !("id" in value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = (value as { id?: unknown }).id;
|
||||
return typeof id === "string" ? id : null;
|
||||
};
|
||||
|
||||
const toDisplayValue = (value: unknown, formatter?: (id: string) => string): string => {
|
||||
const id = extractId(value);
|
||||
if (id && formatter) {
|
||||
return formatter(id);
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return "-";
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
export const advancedCommand = defineCommand({
|
||||
meta: {
|
||||
name: "advanced",
|
||||
category: "utility",
|
||||
},
|
||||
permissions: [PermissionFlagsBits.ManageMessages],
|
||||
args: [
|
||||
{
|
||||
name: "text",
|
||||
type: "string",
|
||||
required: true,
|
||||
descriptionKey: "args.text",
|
||||
},
|
||||
{
|
||||
name: "count",
|
||||
type: "int",
|
||||
required: true,
|
||||
descriptionKey: "args.count",
|
||||
},
|
||||
{
|
||||
name: "ratio",
|
||||
type: "number",
|
||||
required: false,
|
||||
descriptionKey: "args.ratio",
|
||||
},
|
||||
{
|
||||
name: "enabled",
|
||||
type: "boolean",
|
||||
required: false,
|
||||
descriptionKey: "args.enabled",
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
type: "user",
|
||||
required: true,
|
||||
descriptionKey: "args.user",
|
||||
},
|
||||
{
|
||||
name: "channel",
|
||||
type: "channel",
|
||||
required: false,
|
||||
descriptionKey: "args.channel",
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "role",
|
||||
required: false,
|
||||
descriptionKey: "args.role",
|
||||
},
|
||||
],
|
||||
examples: [
|
||||
{
|
||||
args: '"hello world" 5 1.5 true @Arthur #general @Moderators',
|
||||
descriptionKey: "examples.full",
|
||||
},
|
||||
{
|
||||
source: "slash",
|
||||
descriptionKey: "examples.slash",
|
||||
},
|
||||
],
|
||||
execute: async (ctx) => {
|
||||
const responses = (ctx.commandText.responses as Record<string, unknown> | undefined) ?? {};
|
||||
const template = typeof responses.summary === "string"
|
||||
? responses.summary
|
||||
: "text={{text}} count={{count}} ratio={{ratio}} enabled={{enabled}} user={{user}} channel={{channel}} role={{role}} source={{source}}";
|
||||
|
||||
await ctx.reply(
|
||||
ctx.format(template, {
|
||||
text: toDisplayValue(ctx.args.text),
|
||||
count: toDisplayValue(ctx.args.count),
|
||||
ratio: toDisplayValue(ctx.args.ratio),
|
||||
enabled: toDisplayValue(ctx.args.enabled),
|
||||
user: toDisplayValue(ctx.args.user, (id) => `<@${id}>`),
|
||||
channel: toDisplayValue(ctx.args.channel, (id) => `<#${id}>`),
|
||||
role: toDisplayValue(ctx.args.role, (id) => `<@&${id}>`),
|
||||
source: ctx.source,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineCommand } from "../../framework/commands/defineCommand.js";
|
||||
|
||||
export const pingCommand = defineCommand({
|
||||
meta: {
|
||||
name: "ping",
|
||||
category: "utility",
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
descriptionKey: "examples.basic",
|
||||
},
|
||||
],
|
||||
execute: async (ctx) => {
|
||||
const responses = (ctx.commandText.responses as Record<string, unknown> | undefined) ?? {};
|
||||
const template = typeof responses.pong === "string" ? responses.pong : "Pong {{latency}}ms";
|
||||
|
||||
await ctx.reply(
|
||||
ctx.format(template, {
|
||||
latency: ctx.client.ws.ping,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
import type { ChatInputCommandInteraction, Message } from "discord.js";
|
||||
|
||||
import type { CommandArgValue, CommandArgument, TranslationVars } from "../types/command.js";
|
||||
|
||||
export interface ArgumentParseError {
|
||||
key: string;
|
||||
vars?: TranslationVars;
|
||||
}
|
||||
|
||||
export interface ParsedArgumentsResult {
|
||||
values: Record<string, CommandArgValue>;
|
||||
errors: ArgumentParseError[];
|
||||
}
|
||||
|
||||
const USER_MENTION_PATTERN = /^<@!?(\d{16,22})>$/;
|
||||
const USER_ID_PATTERN = /^(\d{16,22})$/;
|
||||
|
||||
const BOOLEAN_TRUE = new Set(["true", "1", "yes", "y", "on"]);
|
||||
const BOOLEAN_FALSE = new Set(["false", "0", "no", "n", "off"]);
|
||||
|
||||
export const tokenizePrefixInput = (raw: string): string[] => {
|
||||
const tokens: string[] = [];
|
||||
const regex = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|(\S+)/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(raw)) !== null) {
|
||||
tokens.push((match[1] ?? match[2] ?? match[3] ?? "").replace(/\\(["'])/g, "$1"));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
};
|
||||
|
||||
export const parsePrefixArgs = async (
|
||||
message: Message,
|
||||
definitions: CommandArgument[],
|
||||
rawInput: string,
|
||||
): Promise<ParsedArgumentsResult> => {
|
||||
const tokens = tokenizePrefixInput(rawInput);
|
||||
const values: Record<string, CommandArgValue> = {};
|
||||
const errors: ArgumentParseError[] = [];
|
||||
|
||||
for (const definition of definitions) {
|
||||
const token = tokens.shift();
|
||||
|
||||
if (!token) {
|
||||
if (definition.required) {
|
||||
errors.push({ key: "errors.args.missing", vars: { arg: definition.name } });
|
||||
}
|
||||
values[definition.name] = undefined;
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = await parseByTypeFromPrefix(message, definition.type, token);
|
||||
if (parsed.ok) {
|
||||
values[definition.name] = parsed.value;
|
||||
continue;
|
||||
}
|
||||
|
||||
errors.push(parsed.error);
|
||||
}
|
||||
|
||||
return { values, errors };
|
||||
};
|
||||
|
||||
export const parseSlashArgs = (
|
||||
interaction: ChatInputCommandInteraction,
|
||||
definitions: CommandArgument[],
|
||||
): ParsedArgumentsResult => {
|
||||
const values: Record<string, CommandArgValue> = {};
|
||||
const errors: ArgumentParseError[] = [];
|
||||
|
||||
for (const definition of definitions) {
|
||||
try {
|
||||
values[definition.name] = parseByTypeFromSlash(interaction, definition);
|
||||
} catch {
|
||||
if (definition.required) {
|
||||
errors.push({ key: "errors.args.missing", vars: { arg: definition.name } });
|
||||
}
|
||||
values[definition.name] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return { values, errors };
|
||||
};
|
||||
|
||||
const parseByTypeFromPrefix = async (
|
||||
message: Message,
|
||||
type: CommandArgument["type"],
|
||||
token: string,
|
||||
): Promise<{ ok: true; value: CommandArgValue } | { ok: false; error: ArgumentParseError }> => {
|
||||
switch (type) {
|
||||
case "string":
|
||||
return { ok: true, value: token };
|
||||
|
||||
case "int": {
|
||||
const value = Number.parseInt(token, 10);
|
||||
if (Number.isNaN(value)) {
|
||||
return { ok: false, error: { key: "errors.args.invalidInt", vars: { value: token } } };
|
||||
}
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
case "number": {
|
||||
const value = Number(token);
|
||||
if (Number.isNaN(value)) {
|
||||
return { ok: false, error: { key: "errors.args.invalidNumber", vars: { value: token } } };
|
||||
}
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
case "boolean": {
|
||||
const normalized = token.toLowerCase();
|
||||
if (BOOLEAN_TRUE.has(normalized)) {
|
||||
return { ok: true, value: true };
|
||||
}
|
||||
if (BOOLEAN_FALSE.has(normalized)) {
|
||||
return { ok: true, value: false };
|
||||
}
|
||||
return { ok: false, error: { key: "errors.args.invalidBoolean", vars: { value: token } } };
|
||||
}
|
||||
|
||||
case "user": {
|
||||
const mentionMatch = token.match(USER_MENTION_PATTERN);
|
||||
const idMatch = token.match(USER_ID_PATTERN);
|
||||
const userId = mentionMatch?.[1] ?? idMatch?.[1];
|
||||
|
||||
if (!userId) {
|
||||
return { ok: false, error: { key: "errors.args.invalidUser", vars: { value: token } } };
|
||||
}
|
||||
|
||||
const fromMention = message.mentions.users.get(userId);
|
||||
if (fromMention) {
|
||||
return { ok: true, value: fromMention };
|
||||
}
|
||||
|
||||
try {
|
||||
const fetched = await message.client.users.fetch(userId);
|
||||
return { ok: true, value: fetched };
|
||||
} catch {
|
||||
return { ok: false, error: { key: "errors.args.invalidUser", vars: { value: token } } };
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return { ok: true, value: token };
|
||||
}
|
||||
};
|
||||
|
||||
const parseByTypeFromSlash = (
|
||||
interaction: ChatInputCommandInteraction,
|
||||
definition: CommandArgument,
|
||||
): CommandArgValue => {
|
||||
switch (definition.type) {
|
||||
case "string":
|
||||
return interaction.options.getString(definition.name, definition.required) ?? undefined;
|
||||
|
||||
case "int":
|
||||
return interaction.options.getInteger(definition.name, definition.required) ?? undefined;
|
||||
|
||||
case "number":
|
||||
return interaction.options.getNumber(definition.name, definition.required) ?? undefined;
|
||||
|
||||
case "boolean":
|
||||
return interaction.options.getBoolean(definition.name, definition.required) ?? undefined;
|
||||
|
||||
case "user":
|
||||
return interaction.options.getUser(definition.name, definition.required) ?? undefined;
|
||||
|
||||
case "channel":
|
||||
return (interaction.options.getChannel(definition.name, definition.required) ?? undefined) as CommandArgValue;
|
||||
|
||||
case "role":
|
||||
return (interaction.options.getRole(definition.name, definition.required) ?? undefined) as CommandArgValue;
|
||||
|
||||
default:
|
||||
return interaction.options.getString(definition.name, definition.required) ?? undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { BotCommand, BotCommandInput } from "../types/command.js";
|
||||
|
||||
export const defineCommand = (input: BotCommandInput): BotCommand => {
|
||||
// Discord requires all required slash options before any optional ones.
|
||||
const normalizedArgs = [...(input.args ?? [])].sort((a, b) => {
|
||||
if (a.required === b.required) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return a.required ? -1 : 1;
|
||||
});
|
||||
|
||||
return {
|
||||
meta: input.meta,
|
||||
args: normalizedArgs,
|
||||
permissions: input.permissions ?? [],
|
||||
examples: input.examples ?? [],
|
||||
execute: input.execute,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { REST, Routes } from "discord.js";
|
||||
|
||||
import { buildSlashPayload } from "./slashBuilder.js";
|
||||
import type { CommandRegistry } from "./registry.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
|
||||
export interface DeployCommandsOptions {
|
||||
token: string;
|
||||
clientId: string;
|
||||
guildId?: string;
|
||||
registry: CommandRegistry;
|
||||
i18n: I18nService;
|
||||
}
|
||||
|
||||
export interface DeployCommandsResult {
|
||||
scope: "guild" | "global";
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const deployApplicationCommands = async (options: DeployCommandsOptions): Promise<DeployCommandsResult> => {
|
||||
const body = buildSlashPayload(options.registry.getAll(), options.i18n);
|
||||
const rest = new REST({ version: "10" }).setToken(options.token);
|
||||
|
||||
if (options.guildId) {
|
||||
await rest.put(Routes.applicationGuildCommands(options.clientId, options.guildId), { body });
|
||||
return { scope: "guild", count: body.length };
|
||||
}
|
||||
|
||||
await rest.put(Routes.applicationCommands(options.clientId), { body });
|
||||
return { scope: "global", count: body.length };
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { BotCommand, PrefixTriggerMatch } from "../types/command.js";
|
||||
import { SUPPORTED_LANGS } from "../types/command.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
|
||||
const normalize = (value: string): string => value.trim().toLowerCase();
|
||||
|
||||
export class CommandRegistry {
|
||||
private readonly commandsByName = new Map<string, BotCommand>();
|
||||
private readonly prefixTriggers = new Map<string, PrefixTriggerMatch>();
|
||||
private readonly slashTriggers = new Map<string, BotCommand>();
|
||||
|
||||
public constructor(
|
||||
commands: BotCommand[],
|
||||
private readonly i18n: I18nService,
|
||||
) {
|
||||
for (const command of commands) {
|
||||
this.register(command);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll(): readonly BotCommand[] {
|
||||
return [...this.commandsByName.values()];
|
||||
}
|
||||
|
||||
public findByName(name: string): BotCommand | undefined {
|
||||
return this.commandsByName.get(normalize(name));
|
||||
}
|
||||
|
||||
public findByAnyPrefixTrigger(trigger: string): PrefixTriggerMatch | undefined {
|
||||
return this.prefixTriggers.get(normalize(trigger));
|
||||
}
|
||||
|
||||
public findBySlashTrigger(trigger: string): BotCommand | undefined {
|
||||
return this.slashTriggers.get(normalize(trigger));
|
||||
}
|
||||
|
||||
private register(command: BotCommand): void {
|
||||
const name = normalize(command.meta.name);
|
||||
if (this.commandsByName.has(name)) {
|
||||
throw new Error(`Duplicate command meta.name: ${command.meta.name}`);
|
||||
}
|
||||
|
||||
this.commandsByName.set(name, command);
|
||||
|
||||
for (const lang of SUPPORTED_LANGS) {
|
||||
const trigger = normalize(this.i18n.commandTrigger(lang, command.meta.name));
|
||||
const existing = this.prefixTriggers.get(trigger);
|
||||
|
||||
if (existing) {
|
||||
if (existing.command.meta.name !== command.meta.name) {
|
||||
throw new Error(`Duplicate prefix trigger: ${trigger}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
this.prefixTriggers.set(trigger, {
|
||||
command,
|
||||
lang,
|
||||
trigger,
|
||||
});
|
||||
}
|
||||
|
||||
const slashKeys = [command.meta.name, ...SUPPORTED_LANGS.map((lang) => this.i18n.commandName(lang, command.meta.name))];
|
||||
|
||||
for (const keyRaw of slashKeys) {
|
||||
const key = normalize(keyRaw);
|
||||
const existing = this.slashTriggers.get(key);
|
||||
|
||||
if (existing && existing.meta.name !== command.meta.name) {
|
||||
throw new Error(`Duplicate slash trigger: ${key}`);
|
||||
}
|
||||
|
||||
this.slashTriggers.set(key, command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
ApplicationCommandOptionType,
|
||||
SlashCommandBuilder,
|
||||
type RESTPostAPIChatInputApplicationCommandsJSONBody,
|
||||
} from "discord.js";
|
||||
|
||||
import type { BotCommand, CommandArgument, SupportedLang } from "../types/command.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
|
||||
const LANG_TO_DISCORD_LOCALE: Record<SupportedLang, string> = {
|
||||
en: "en-US",
|
||||
fr: "fr",
|
||||
es: "es-ES",
|
||||
};
|
||||
|
||||
const toLocalizationMap = (source: Partial<Record<SupportedLang, string>>): Record<string, string> => {
|
||||
const entries = Object.entries(source)
|
||||
.filter(([, value]) => Boolean(value))
|
||||
.map(([lang, value]) => [LANG_TO_DISCORD_LOCALE[lang as SupportedLang], value as string]);
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
const argDescriptionKey = (command: BotCommand, arg: CommandArgument): string => {
|
||||
return `commands.${command.meta.name}.${arg.descriptionKey}`;
|
||||
};
|
||||
|
||||
const commandDescriptionKey = (command: BotCommand): string => {
|
||||
return `commands.${command.meta.name}.description`;
|
||||
};
|
||||
|
||||
const buildHelpCommandChoices = (commands: readonly BotCommand[], i18n: I18nService): Array<{ name: string; value: string }> => {
|
||||
return commands
|
||||
.slice()
|
||||
.sort((a, b) => a.meta.name.localeCompare(b.meta.name))
|
||||
.slice(0, 25)
|
||||
.map((cmd) => ({
|
||||
name: i18n.commandName("en", cmd.meta.name),
|
||||
value: cmd.meta.name,
|
||||
}));
|
||||
};
|
||||
|
||||
const applyOption = (
|
||||
builder: SlashCommandBuilder,
|
||||
command: BotCommand,
|
||||
arg: CommandArgument,
|
||||
i18n: I18nService,
|
||||
allCommands: readonly BotCommand[],
|
||||
): void => {
|
||||
const descriptionKey = argDescriptionKey(command, arg);
|
||||
const descriptionEn = i18n.t("en", descriptionKey);
|
||||
const descriptionLocalizations = toLocalizationMap({
|
||||
en: i18n.t("en", descriptionKey),
|
||||
fr: i18n.t("fr", descriptionKey),
|
||||
es: i18n.t("es", descriptionKey),
|
||||
});
|
||||
|
||||
if (arg.type === "user") {
|
||||
builder.addUserOption((opt) =>
|
||||
opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.type === "int") {
|
||||
builder.addIntegerOption((opt) =>
|
||||
opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.type === "number") {
|
||||
builder.addNumberOption((opt) =>
|
||||
opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.type === "boolean") {
|
||||
builder.addBooleanOption((opt) =>
|
||||
opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.type === "channel") {
|
||||
builder.addChannelOption((opt) =>
|
||||
opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (arg.type === "role") {
|
||||
builder.addRoleOption((opt) =>
|
||||
opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.addStringOption((opt) =>
|
||||
{
|
||||
const configured = opt
|
||||
.setName(arg.name)
|
||||
.setDescription(descriptionEn)
|
||||
.setDescriptionLocalizations(descriptionLocalizations)
|
||||
.setRequired(arg.required);
|
||||
|
||||
if (command.meta.name === "help" && arg.name === "command") {
|
||||
const choices = buildHelpCommandChoices(allCommands, i18n);
|
||||
if (choices.length > 0) {
|
||||
configured.addChoices(...choices);
|
||||
}
|
||||
}
|
||||
|
||||
return configured;
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const buildSlashPayload = (
|
||||
commands: readonly BotCommand[],
|
||||
i18n: I18nService,
|
||||
): RESTPostAPIChatInputApplicationCommandsJSONBody[] => {
|
||||
return commands.map((command) => {
|
||||
const descriptionKey = commandDescriptionKey(command);
|
||||
|
||||
const slashLocalizations = toLocalizationMap({
|
||||
fr: i18n.commandName("fr", command.meta.name),
|
||||
es: i18n.commandName("es", command.meta.name),
|
||||
});
|
||||
|
||||
const slashBuilder = new SlashCommandBuilder()
|
||||
.setName(command.meta.name)
|
||||
.setDescription(i18n.t("en", descriptionKey))
|
||||
.setDescriptionLocalizations(
|
||||
toLocalizationMap({
|
||||
en: i18n.t("en", descriptionKey),
|
||||
fr: i18n.t("fr", descriptionKey),
|
||||
es: i18n.t("es", descriptionKey),
|
||||
}),
|
||||
)
|
||||
.setNameLocalizations(slashLocalizations);
|
||||
|
||||
for (const arg of command.args) {
|
||||
applyOption(slashBuilder, command, arg, i18n, commands);
|
||||
}
|
||||
|
||||
return slashBuilder.toJSON() as RESTPostAPIChatInputApplicationCommandsJSONBody;
|
||||
});
|
||||
};
|
||||
|
||||
export const commandOptionType = (type: CommandArgument["type"]): ApplicationCommandOptionType => {
|
||||
switch (type) {
|
||||
case "user":
|
||||
return ApplicationCommandOptionType.User;
|
||||
case "int":
|
||||
return ApplicationCommandOptionType.Integer;
|
||||
case "number":
|
||||
return ApplicationCommandOptionType.Number;
|
||||
case "boolean":
|
||||
return ApplicationCommandOptionType.Boolean;
|
||||
case "channel":
|
||||
return ApplicationCommandOptionType.Channel;
|
||||
case "role":
|
||||
return ApplicationCommandOptionType.Role;
|
||||
default:
|
||||
return ApplicationCommandOptionType.String;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { BotCommand, CommandI18nTools, SupportedLang } from "../types/command.js";
|
||||
|
||||
const formatArgToken = (name: string, required: boolean): string => required ? `<${name}>` : `[${name}]`;
|
||||
|
||||
export const resolvePrefixTrigger = (
|
||||
command: BotCommand,
|
||||
lang: SupportedLang,
|
||||
defaultLang: SupportedLang,
|
||||
i18n: CommandI18nTools,
|
||||
): string => {
|
||||
const fromLang = i18n.commandTrigger(lang, command.meta.name);
|
||||
if (fromLang) {
|
||||
return fromLang;
|
||||
}
|
||||
|
||||
const fromDefault = i18n.commandTrigger(defaultLang, command.meta.name);
|
||||
if (fromDefault) {
|
||||
return fromDefault;
|
||||
}
|
||||
|
||||
return command.meta.name;
|
||||
};
|
||||
|
||||
export const resolveSlashName = (command: BotCommand, lang: SupportedLang, i18n: CommandI18nTools): string => {
|
||||
return i18n.commandName(lang, command.meta.name);
|
||||
};
|
||||
|
||||
export const buildPrefixUsage = (
|
||||
command: BotCommand,
|
||||
prefix: string,
|
||||
lang: SupportedLang,
|
||||
defaultLang: SupportedLang,
|
||||
i18n: CommandI18nTools,
|
||||
): string => {
|
||||
const trigger = resolvePrefixTrigger(command, lang, defaultLang, i18n);
|
||||
const args = command.args.map((arg) => formatArgToken(arg.name, arg.required)).join(" ");
|
||||
return `${prefix}${trigger}${args.length > 0 ? ` ${args}` : ""}`;
|
||||
};
|
||||
|
||||
export const buildSlashUsage = (command: BotCommand, lang: SupportedLang, i18n: CommandI18nTools): string => {
|
||||
const slashName = resolveSlashName(command, lang, i18n);
|
||||
const args = command.args.map((arg) => formatArgToken(arg.name, arg.required)).join(" ");
|
||||
return `/${slashName}${args.length > 0 ? ` ${args}` : ""}`;
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { config as loadEnv } from "dotenv";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SUPPORTED_LANGS } from "../types/command.js";
|
||||
|
||||
loadEnv();
|
||||
|
||||
const envSchema = z.object({
|
||||
DISCORD_TOKEN: z.string().min(1, "DISCORD_TOKEN is required"),
|
||||
DISCORD_CLIENT_ID: z.string().min(1, "DISCORD_CLIENT_ID is required"),
|
||||
PREFIX: z.string().min(1).max(5).default("+"),
|
||||
DEFAULT_LANG: z.enum(SUPPORTED_LANGS).default("en"),
|
||||
DEV_GUILD_ID: z.string().optional().transform((value) => value && value.length > 0 ? value : undefined),
|
||||
AUTO_DEPLOY_SLASH: z
|
||||
.string()
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((value) => value.toLowerCase() === "true"),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
PermissionsBitField,
|
||||
type ChatInputCommandInteraction,
|
||||
type Message,
|
||||
type PermissionResolvable,
|
||||
} from "discord.js";
|
||||
|
||||
import type { BotCommand, CommandExecutionContext } from "../types/command.js";
|
||||
|
||||
export class CommandExecutor {
|
||||
public async run(command: BotCommand, ctx: CommandExecutionContext): Promise<void> {
|
||||
const missingUserPermissions = this.getMissingPermissions(command.permissions, this.memberPermissions(ctx));
|
||||
if (missingUserPermissions.length > 0) {
|
||||
await ctx.reply(ctx.t("errors.permissions.user", { permissions: missingUserPermissions.join(", ") }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(ctx);
|
||||
} catch (error) {
|
||||
console.error(`[command:${command.meta.name}] execution failed`, error);
|
||||
await ctx.reply(ctx.t("errors.execution"));
|
||||
}
|
||||
}
|
||||
|
||||
private memberPermissions(ctx: CommandExecutionContext): Readonly<PermissionsBitField> | null {
|
||||
if (ctx.source === "slash") {
|
||||
return (ctx.raw as ChatInputCommandInteraction).memberPermissions ?? null;
|
||||
}
|
||||
|
||||
const message = ctx.raw as Message;
|
||||
return message.member?.permissions ?? null;
|
||||
}
|
||||
|
||||
private getMissingPermissions(
|
||||
required: PermissionResolvable[],
|
||||
available: Readonly<PermissionsBitField> | null,
|
||||
): string[] {
|
||||
if (required.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!available) {
|
||||
return [...new Set(required.flatMap((permission) => this.permissionToLabels(permission)))];
|
||||
}
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
required
|
||||
.filter((permission) => !available.has(permission))
|
||||
.flatMap((permission) => this.permissionToLabels(permission)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private permissionToLabels(permission: PermissionResolvable): string[] {
|
||||
try {
|
||||
const resolved = PermissionsBitField.resolve(permission);
|
||||
const labels = new PermissionsBitField(resolved).toArray().map(this.formatPermissionLabel);
|
||||
if (labels.length > 0) {
|
||||
return labels;
|
||||
}
|
||||
} catch {
|
||||
// Fallback handled below.
|
||||
}
|
||||
|
||||
return [String(permission)];
|
||||
}
|
||||
|
||||
private formatPermissionLabel(permission: string): string {
|
||||
return permission
|
||||
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
||||
.replace(/_/g, " ")
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { InteractionReplyOptions, Message, MessageReplyOptions } from "discord.js";
|
||||
|
||||
import { parsePrefixArgs } from "../commands/argParser.js";
|
||||
import { buildPrefixUsage } from "../commands/usage.js";
|
||||
import type { CommandRegistry } from "../commands/registry.js";
|
||||
import { CommandExecutor } from "../execution/CommandExecutor.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
import type { ReplyPayload, SupportedLang } from "../types/command.js";
|
||||
|
||||
interface PrefixHandlerDeps {
|
||||
registry: CommandRegistry;
|
||||
i18n: I18nService;
|
||||
executor: CommandExecutor;
|
||||
prefix: string;
|
||||
defaultLang: SupportedLang;
|
||||
}
|
||||
|
||||
const toMessageReplyOptions = (payload: Exclude<ReplyPayload, string>): MessageReplyOptions => {
|
||||
const {
|
||||
ephemeral: _ephemeral,
|
||||
fetchReply: _fetchReply,
|
||||
withResponse: _withResponse,
|
||||
flags: _flags,
|
||||
...rest
|
||||
} = payload as InteractionReplyOptions & MessageReplyOptions;
|
||||
|
||||
return rest as MessageReplyOptions;
|
||||
};
|
||||
|
||||
export const createPrefixHandler = (deps: PrefixHandlerDeps) => {
|
||||
return async (message: Message): Promise<void> => {
|
||||
if (message.author.bot || !message.content.startsWith(deps.prefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = message.content.slice(deps.prefix.length).trim();
|
||||
if (!content) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstSpaceIndex = content.indexOf(" ");
|
||||
const trigger = (firstSpaceIndex === -1 ? content : content.slice(0, firstSpaceIndex)).toLowerCase();
|
||||
const rawArgs = firstSpaceIndex === -1 ? "" : content.slice(firstSpaceIndex + 1);
|
||||
|
||||
const match = deps.registry.findByAnyPrefixTrigger(trigger);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = match.command;
|
||||
const lang = match.lang;
|
||||
const t = (key: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.t(lang, key, vars);
|
||||
|
||||
const parsed = await parsePrefixArgs(message, command.args, rawArgs);
|
||||
if (parsed.errors.length > 0) {
|
||||
const firstError = parsed.errors[0];
|
||||
if (!firstError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usage = buildPrefixUsage(command, deps.prefix, lang, deps.defaultLang, deps.i18n);
|
||||
await message.reply(t(firstError.key, { ...(firstError.vars ?? {}), usage }));
|
||||
return;
|
||||
}
|
||||
|
||||
const ct = (relativeKey: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.commandT(lang, command.meta.name, relativeKey, vars);
|
||||
|
||||
await deps.executor.run(command, {
|
||||
client: message.client,
|
||||
user: message.author,
|
||||
guild: message.guild,
|
||||
channel: message.channel,
|
||||
lang,
|
||||
args: parsed.values,
|
||||
command,
|
||||
source: "prefix",
|
||||
t,
|
||||
ct,
|
||||
commandText: deps.i18n.commandObject(lang, command.meta.name),
|
||||
format: (template, vars) => deps.i18n.format(template, vars),
|
||||
i18n: deps.i18n,
|
||||
raw: message,
|
||||
reply: (payload: ReplyPayload) =>
|
||||
typeof payload === "string"
|
||||
? message.reply(payload)
|
||||
: message.reply(toMessageReplyOptions(payload)),
|
||||
registry: deps.registry,
|
||||
prefix: deps.prefix,
|
||||
defaultLang: deps.defaultLang,
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { ChatInputCommandInteraction } from "discord.js";
|
||||
|
||||
import { parseSlashArgs } from "../commands/argParser.js";
|
||||
import { buildSlashUsage } from "../commands/usage.js";
|
||||
import type { CommandRegistry } from "../commands/registry.js";
|
||||
import { CommandExecutor } from "../execution/CommandExecutor.js";
|
||||
import type { I18nService } from "../i18n/I18nService.js";
|
||||
import type { ReplyPayload, SupportedLang } from "../types/command.js";
|
||||
|
||||
interface SlashHandlerDeps {
|
||||
registry: CommandRegistry;
|
||||
i18n: I18nService;
|
||||
executor: CommandExecutor;
|
||||
prefix: string;
|
||||
defaultLang: SupportedLang;
|
||||
}
|
||||
|
||||
const resolveReply = async (interaction: ChatInputCommandInteraction, payload: ReplyPayload): Promise<unknown> => {
|
||||
if (typeof payload === "string") {
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
return interaction.followUp({ content: payload });
|
||||
}
|
||||
|
||||
return interaction.reply({ content: payload });
|
||||
}
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
return interaction.followUp(payload as never);
|
||||
}
|
||||
|
||||
return interaction.reply(payload as never);
|
||||
};
|
||||
|
||||
export const createSlashHandler = (deps: SlashHandlerDeps) => {
|
||||
return async (interaction: ChatInputCommandInteraction): Promise<void> => {
|
||||
const command = deps.registry.findBySlashTrigger(interaction.commandName);
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = deps.i18n.resolveLang(interaction.locale ?? interaction.guildLocale);
|
||||
const t = (key: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.t(lang, key, vars);
|
||||
|
||||
const parsed = parseSlashArgs(interaction, command.args);
|
||||
if (parsed.errors.length > 0) {
|
||||
const firstError = parsed.errors[0];
|
||||
if (!firstError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const usage = buildSlashUsage(command, lang, deps.i18n);
|
||||
await resolveReply(interaction, {
|
||||
content: t(firstError.key, { ...(firstError.vars ?? {}), usage }),
|
||||
ephemeral: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ct = (relativeKey: string, vars?: Record<string, string | number | boolean | null | undefined>): string =>
|
||||
deps.i18n.commandT(lang, command.meta.name, relativeKey, vars);
|
||||
|
||||
await deps.executor.run(command, {
|
||||
client: interaction.client,
|
||||
user: interaction.user,
|
||||
guild: interaction.guild,
|
||||
channel: interaction.channel,
|
||||
lang,
|
||||
args: parsed.values,
|
||||
command,
|
||||
source: "slash",
|
||||
t,
|
||||
ct,
|
||||
commandText: deps.i18n.commandObject(lang, command.meta.name),
|
||||
format: (template, vars) => deps.i18n.format(template, vars),
|
||||
i18n: deps.i18n,
|
||||
raw: interaction,
|
||||
reply: (payload: ReplyPayload) => resolveReply(interaction, payload),
|
||||
registry: deps.registry,
|
||||
prefix: deps.prefix,
|
||||
defaultLang: deps.defaultLang,
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { SUPPORTED_LANGS, type SupportedLang, type TranslationVars } from "../types/command.js";
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
const DISCORD_LOCALE_MAP: Record<string, SupportedLang> = {
|
||||
en: "en",
|
||||
"en-us": "en",
|
||||
"en-gb": "en",
|
||||
fr: "fr",
|
||||
"fr-fr": "fr",
|
||||
es: "es",
|
||||
"es-es": "es",
|
||||
"es-419": "es",
|
||||
};
|
||||
|
||||
export class I18nService {
|
||||
private readonly dictionaries: Record<SupportedLang, JsonObject>;
|
||||
|
||||
public constructor(private readonly defaultLang: SupportedLang) {
|
||||
this.dictionaries = this.loadDictionaries();
|
||||
}
|
||||
|
||||
public resolveLang(input?: string | null): SupportedLang {
|
||||
if (!input) {
|
||||
return this.defaultLang;
|
||||
}
|
||||
|
||||
const normalized = input.toLowerCase();
|
||||
|
||||
const direct = DISCORD_LOCALE_MAP[normalized];
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const short = normalized.split("-")[0];
|
||||
const fromShort = short ? DISCORD_LOCALE_MAP[short] : undefined;
|
||||
if (fromShort) {
|
||||
return fromShort;
|
||||
}
|
||||
|
||||
return this.defaultLang;
|
||||
}
|
||||
|
||||
public t(lang: SupportedLang, key: string, vars: TranslationVars = {}): string {
|
||||
const fromLang = this.lookup(this.dictionaries[lang], key);
|
||||
const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key);
|
||||
|
||||
const template = typeof fromLang === "string" ? fromLang : typeof fromDefault === "string" ? fromDefault : key;
|
||||
|
||||
return this.format(template, vars);
|
||||
}
|
||||
|
||||
public commandT(lang: SupportedLang, commandName: string, relativeKey: string, vars: TranslationVars = {}): string {
|
||||
return this.t(lang, `${this.commandBaseKey(commandName)}.${relativeKey}`, vars);
|
||||
}
|
||||
|
||||
public commandName(lang: SupportedLang, commandName: string): string {
|
||||
const key = `${this.commandBaseKey(commandName)}.name`;
|
||||
const fromLang = this.lookup(this.dictionaries[lang], key);
|
||||
if (typeof fromLang === "string" && fromLang.length > 0) {
|
||||
return fromLang;
|
||||
}
|
||||
|
||||
const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key);
|
||||
if (typeof fromDefault === "string" && fromDefault.length > 0) {
|
||||
return fromDefault;
|
||||
}
|
||||
|
||||
return commandName;
|
||||
}
|
||||
|
||||
public commandTrigger(lang: SupportedLang, commandName: string): string {
|
||||
return this.commandName(lang, commandName).trim().toLowerCase();
|
||||
}
|
||||
|
||||
public commandObject(lang: SupportedLang, commandName: string): Record<string, unknown> {
|
||||
const key = this.commandBaseKey(commandName);
|
||||
const fromLang = this.lookup(this.dictionaries[lang], key);
|
||||
if (this.isObject(fromLang)) {
|
||||
return fromLang;
|
||||
}
|
||||
|
||||
const fromDefault = this.lookup(this.dictionaries[this.defaultLang], key);
|
||||
if (this.isObject(fromDefault)) {
|
||||
return fromDefault;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
public format(template: string, vars: TranslationVars = {}): string {
|
||||
return this.interpolate(template, vars);
|
||||
}
|
||||
|
||||
private loadDictionaries(): Record<SupportedLang, JsonObject> {
|
||||
const localeDir = path.join(process.cwd(), "locales");
|
||||
|
||||
return SUPPORTED_LANGS.reduce<Record<SupportedLang, JsonObject>>((acc, lang) => {
|
||||
const filePath = path.join(localeDir, `${lang}.json`);
|
||||
const raw = readFileSync(filePath, "utf-8");
|
||||
acc[lang] = JSON.parse(raw) as JsonObject;
|
||||
return acc;
|
||||
}, {} as Record<SupportedLang, JsonObject>);
|
||||
}
|
||||
|
||||
private lookup(source: JsonObject, key: string): unknown {
|
||||
const parts = key.split(".");
|
||||
let current: unknown = source;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
current = (current as JsonObject)[part];
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private commandBaseKey(commandName: string): string {
|
||||
return `commands.${commandName}`;
|
||||
}
|
||||
|
||||
private isObject(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private interpolate(template: string, vars: TranslationVars): string {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (_, variable: string) => {
|
||||
const value = vars[variable];
|
||||
return value === undefined || value === null ? "" : String(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type {
|
||||
ChatInputCommandInteraction,
|
||||
Client,
|
||||
Guild,
|
||||
GuildBasedChannel,
|
||||
GuildMember,
|
||||
InteractionReplyOptions,
|
||||
Message,
|
||||
MessageCreateOptions,
|
||||
MessageReplyOptions,
|
||||
PermissionResolvable,
|
||||
Role,
|
||||
TextBasedChannel,
|
||||
User,
|
||||
} from "discord.js";
|
||||
|
||||
export const SUPPORTED_LANGS = ["en", "fr", "es"] as const;
|
||||
|
||||
export type SupportedLang = (typeof SUPPORTED_LANGS)[number];
|
||||
export type CommandSource = "prefix" | "slash";
|
||||
export type CommandArgType = "string" | "user" | "int" | "boolean" | "number" | "channel" | "role";
|
||||
|
||||
export type TranslationVars = Record<string, string | number | boolean | null | undefined>;
|
||||
export type ReplyPayload = string | MessageCreateOptions | MessageReplyOptions | InteractionReplyOptions;
|
||||
|
||||
export type CommandArgValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| User
|
||||
| GuildMember
|
||||
| Role
|
||||
| GuildBasedChannel
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
export interface CommandMeta {
|
||||
name: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface CommandArgument {
|
||||
name: string;
|
||||
type: CommandArgType;
|
||||
required: boolean;
|
||||
descriptionKey: string;
|
||||
}
|
||||
|
||||
export interface CommandExample {
|
||||
source?: CommandSource;
|
||||
args?: string;
|
||||
descriptionKey: string;
|
||||
}
|
||||
|
||||
export interface PrefixTriggerMatch {
|
||||
command: BotCommand;
|
||||
lang: SupportedLang;
|
||||
trigger: string;
|
||||
}
|
||||
|
||||
export interface CommandRegistryReader {
|
||||
getAll(): readonly BotCommand[];
|
||||
findByName(name: string): BotCommand | undefined;
|
||||
findByAnyPrefixTrigger(trigger: string): PrefixTriggerMatch | undefined;
|
||||
}
|
||||
|
||||
export interface CommandI18nTools {
|
||||
commandName: (lang: SupportedLang, commandName: string) => string;
|
||||
commandTrigger: (lang: SupportedLang, commandName: string) => string;
|
||||
commandT: (lang: SupportedLang, commandName: string, relativeKey: string, vars?: TranslationVars) => string;
|
||||
commandObject: (lang: SupportedLang, commandName: string) => Record<string, unknown>;
|
||||
format: (template: string, vars?: TranslationVars) => string;
|
||||
}
|
||||
|
||||
export interface CommandExecutionContext {
|
||||
client: Client;
|
||||
user: User;
|
||||
guild: Guild | null;
|
||||
channel: TextBasedChannel | null;
|
||||
lang: SupportedLang;
|
||||
args: Record<string, CommandArgValue>;
|
||||
command: BotCommand;
|
||||
source: CommandSource;
|
||||
t: (key: string, vars?: TranslationVars) => string;
|
||||
ct: (relativeKey: string, vars?: TranslationVars) => string;
|
||||
commandText: Record<string, unknown>;
|
||||
format: (template: string, vars?: TranslationVars) => string;
|
||||
i18n: CommandI18nTools;
|
||||
raw: Message | ChatInputCommandInteraction;
|
||||
reply: (payload: ReplyPayload) => Promise<unknown>;
|
||||
registry: CommandRegistryReader;
|
||||
prefix: string;
|
||||
defaultLang: SupportedLang;
|
||||
}
|
||||
|
||||
export interface BotCommandInput {
|
||||
meta: CommandMeta;
|
||||
args?: CommandArgument[];
|
||||
permissions?: PermissionResolvable[];
|
||||
examples?: CommandExample[];
|
||||
execute: (ctx: CommandExecutionContext) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface BotCommand {
|
||||
meta: CommandMeta;
|
||||
args: CommandArgument[];
|
||||
permissions: PermissionResolvable[];
|
||||
examples: CommandExample[];
|
||||
execute: (ctx: CommandExecutionContext) => Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Client, Events, GatewayIntentBits } from "discord.js";
|
||||
|
||||
import { commandList } from "./commands/index.js";
|
||||
import { deployApplicationCommands } from "./framework/commands/deploy.js";
|
||||
import { CommandRegistry } from "./framework/commands/registry.js";
|
||||
import { env } from "./framework/config/env.js";
|
||||
import { CommandExecutor } from "./framework/execution/CommandExecutor.js";
|
||||
import { createPrefixHandler } from "./framework/handlers/prefixHandler.js";
|
||||
import { createSlashHandler } from "./framework/handlers/slashHandler.js";
|
||||
import { I18nService } from "./framework/i18n/I18nService.js";
|
||||
|
||||
const bootstrap = async (): Promise<void> => {
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent],
|
||||
});
|
||||
|
||||
const i18n = new I18nService(env.DEFAULT_LANG);
|
||||
const registry = new CommandRegistry(commandList, i18n);
|
||||
const executor = new CommandExecutor();
|
||||
|
||||
const onPrefixMessage = createPrefixHandler({
|
||||
registry,
|
||||
i18n,
|
||||
executor,
|
||||
prefix: env.PREFIX,
|
||||
defaultLang: env.DEFAULT_LANG,
|
||||
});
|
||||
|
||||
const onSlashInteraction = createSlashHandler({
|
||||
registry,
|
||||
i18n,
|
||||
executor,
|
||||
prefix: env.PREFIX,
|
||||
defaultLang: env.DEFAULT_LANG,
|
||||
});
|
||||
|
||||
client.on(Events.MessageCreate, onPrefixMessage);
|
||||
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await onSlashInteraction(interaction);
|
||||
});
|
||||
|
||||
client.once(Events.ClientReady, async () => {
|
||||
console.log(`[ready] logged as ${client.user?.tag ?? "unknown"}`);
|
||||
|
||||
if (env.AUTO_DEPLOY_SLASH) {
|
||||
try {
|
||||
const result = await deployApplicationCommands({
|
||||
token: env.DISCORD_TOKEN,
|
||||
clientId: env.DISCORD_CLIENT_ID,
|
||||
registry,
|
||||
i18n,
|
||||
...(env.DEV_GUILD_ID ? { guildId: env.DEV_GUILD_ID } : {}),
|
||||
});
|
||||
console.log(`[ready] slash sync done (${result.scope}, ${result.count} commands)`);
|
||||
} catch (error) {
|
||||
console.error("[ready] slash sync failed", error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await client.login(env.DISCORD_TOKEN);
|
||||
};
|
||||
|
||||
bootstrap().catch((error) => {
|
||||
console.error("[boot] fatal error", error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { commandList } from "../commands/index.js";
|
||||
import { deployApplicationCommands } from "../framework/commands/deploy.js";
|
||||
import { CommandRegistry } from "../framework/commands/registry.js";
|
||||
import { env } from "../framework/config/env.js";
|
||||
import { I18nService } from "../framework/i18n/I18nService.js";
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
const i18n = new I18nService(env.DEFAULT_LANG);
|
||||
const registry = new CommandRegistry(commandList, i18n);
|
||||
|
||||
const result = await deployApplicationCommands({
|
||||
token: env.DISCORD_TOKEN,
|
||||
clientId: env.DISCORD_CLIENT_ID,
|
||||
registry,
|
||||
i18n,
|
||||
...(env.DEV_GUILD_ID ? { guildId: env.DEV_GUILD_ID } : {}),
|
||||
});
|
||||
|
||||
console.log(`[deploy] ${result.count} commands deployed (${result.scope})`);
|
||||
};
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("[deploy] failed", error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user