Uma estrutura de bot para Discord escrita em TypeScript, baseada em discord.js, com arquitetura dividida em duas packages:
- core: infraestrutura e abstrações (decorators, handlers, conexão com MongoDB, utilitários, builders personalizados).
- bot: definição de comandos, eventos e interações, consumindo tudo do
core.
Essa separação torna o código limpo, testável, reutilizável e fácil de escalar.
- Principais Recursos
- Arquitetura
- Estrutura de Diretórios
- Instalação
- Configuração
- Uso
- Como Contribuir
- Licença
- Decorators para comandos, eventos e components (v1 e V2)
- Builders customizados (Embed, Button, SelectMenu, Modal, TextInput, V2 Components)
- Handlers automáticos: scan de diretórios e registro de comandos/eventos/interações
- MongoDB integrado via Mongoose
- Organização core / bot para máxima reutilização
- Suporte a Object Calisthenics e clean code
-
core
decorators/: marcações para comandos (@Command), eventos (@Event), components (@EmbedConstructor,@ButtonConstructor,@TextInputConstructor, V2 etc.)handlers/: scan e registro automático de comandos, eventos e interaçõesdatabase/: conexão e models Mongooseutils/: Logger, wrappers de operações comuns
-
bot
commands/: classes de comando que usam os decorators do coreevents/: classes de evento (ready, messageCreate etc.)interactions/: handlers de interações (botões, selects, modais)
/my-discord-bot
├─ packages
│ ├─ core
│ │ ├─ decorators
│ │ │ ├─ builders
│ │ │ │ ├─ EmbedBuilder.ts
│ │ │ │ ├─ ButtonBuilder.ts
│ │ │ │ ├─ TextInputBuilder.ts
│ │ │ │ └─ v2/
│ │ │ │ ├─ BuilderConstructorV2.ts
│ │ │ │ ├─ TextDisplayBuilder.ts
│ │ │ │ └─ …
│ │ │ ├─ Command.ts
│ │ │ ├─ Event.ts
│ │ │ └─ index.ts
│ │ ├─ handlers
│ │ │ ├─ CommandHandler.ts
│ │ │ ├─ EventHandler.ts
│ │ │ └─ InteractionHandler.ts
│ │ ├─ database
│ │ │ ├─ Database.ts
│ │ │ └─ models/User.ts
│ │ └─ utils
│ │ └─ Logger.ts
│ └─ bot
│ ├─ config.ts
│ ├─ DiscordBot.ts
│ ├─ commands
│ │ └─ PingCommand.ts
│ ├─ events
│ │ └─ ReadyEvent.ts
│ └─ interactions
│ └─ ButtonInteractionHandler.ts
├─ package.json
├─ tsconfig.json
├─ .env
└─ index.ts # Bootstrap principal
- Clone o repositório:
git clone https://github.com/seu-usuario/questy-bot.git cd questy-bot - Instale dependências (Bun/Yarn/NPM):
bun install # ou yarn # ou npm install
Crie um arquivo .env na raiz, com:
DISCORD_TOKEN=seu_token_do_discord
MONGO_URI=seu_uri_do_mongodb// packages/bot/commands/PingCommand.ts
import { ButtonBuilder, ChatInputCommandInteraction, EmbedBuilder } from "discord.js";
import { Command } from "@core/decorators/Command";
import type { ICommand } from "@core/handlers/CommandHandler";
import { EmbedConstructor } from "@core/decorators/builders/EmbedBuilder";
import { ButtonConstructor } from "@core/decorators/builders/ButtonBuilder";
import { buildComponentRows } from "@core/handlers/ComponentHandler";
@Command({
name: "ping",
description: "Responde com Pong!",
aliases: ["p"]
})
export default class PingCommand implements ICommand {
@EmbedConstructor({
title: "🏓 Pong!",
description: "Resposta via embed",
color: 0x00ff00
})
embed!: EmbedBuilder;
@ButtonConstructor({
label: "Clique aqui!",
custom_id: "btn_click",
style: 1
})
button!: ButtonBuilder;
public async execute(interaction: ChatInputCommandInteraction) {
await interaction.reply({
embeds: [this.embed],
components: buildComponentRows(this.button)
});
}
}// packages/bot/commands/TestCommand.ts
import { Command } from "@core/decorators/Command";
import type { ICommand } from "@core/handlers/CommandHandler";
import {
V2Message,
TextDisplay,
Separator,
Button,
StringSelect,
UserSelect,
RoleSelect,
MentionableSelect,
ChannelSelect
} from "@core/decorators/builders/v2";
import type { ChatInputCommandInteraction } from "discord.js";
import { responseV2 } from "@core/responses/v2";
@V2Message()
@Command({ name: "test", description: "Teste V2 com vários componentes" })
export default class TestCommand implements ICommand {
@TextDisplay({ content: "🚀 Hello V2!" })
@Separator()
@Button({ customId: "btn_ok", label: "👍 OK", style: 1 })
@StringSelect({
customId: "sel_str",
options: [
{ label: "One", value: "1" },
{ label: "Two", value: "2" }
]
})
@UserSelect({ customId: "sel_user" })
@RoleSelect({ customId: "sel_role" })
@MentionableSelect({ customId: "sel_mention" })
@ChannelSelect({ customId: "sel_channel" })
public async execute(interaction: ChatInputCommandInteraction) {
await responseV2(this.constructor, interaction, "Aqui está o layout V2 completo!");
}
}// packages/bot/events/ReadyEvent.ts
import { Client } from "discord.js";
import { Event } from "@core/decorators/Event";
import type { IEvent } from "@core/handlers/EventHandler";
import { Logger } from "@core/utils/Logger";
@Event({ eventName: "ready", once: true })
export default class ReadyEvent implements IEvent {
public async execute(client: Client): Promise<void> {
Logger.info(`Bot online como ${client.user?.tag}`);
}
}// packages/bot/interactions/ButtonInteractionHandler.ts
import { ButtonInteraction, MessageFlags } from "discord.js";
import { Button } from "@core/decorators/builders/ButtonBuilder";
import type { IComponentInteraction } from "@core/handlers/InteractionHandler";
@Button({ customId: "btn_click" })
export default class ButtonInteractionHandler implements IComponentInteraction {
public async execute(interaction: ButtonInteraction): Promise<void> {
await interaction.reply({
content: "Você clicou no botão! 🎉",
flags: MessageFlags.Ephemeral
});
}
}- Fork este repositório
- Crie uma branch
feature/nome-da-feature - Implemente sua feature e escreva testes
- Abra um Pull Request
- Siga as guidelines de estilo e mantenha o core separado conforme a arquitetura
Este projeto está sob a licença MIT. Veja o arquivo LICENSE para mais detalhes.